More

    React MVC: Implementation With Examples

    Introduction About React MVC:

    The main feature of the Model View Controller (MVC) is separating presentation from the domain. MVC is an object-oriented programming pattern and react is the ‘v’ in MVC.

     Implementing MVC Patterns in React- 

     The MVC pattern described below breaks down into the following two pillars- 

    1)    A Presentation Layer of Controller and View React Components

    2)    A UI-Agnostic Data Model

     

    Pillar 1- Presentation Layer of Controller and View React Components

     The pillar is about separating components by their role regarding access/knowledge of domain objects and logic. So, to say, we are categorizing components by what we know about and what they can do. 

     

    These components can be grouped into two categories: 

    1)    Controller Components

    2)    View Components

     

    What is a controller component? 

    The controller component has a lot of information about the rest of the world, It knows how to access and update “domain data” and how to choose and execute “domain logic”. 

    For example, a controller component may know how to query and mutate data via a RESTful API or read/update objects stored in React Context. 

    In a general sense, controllers are aware of context, side effects, and domains, which are the application, state, and behavior. 

     

    What is a view component? 

    In contrast to a controller component, this is agnostic of most things a controller would know about. A view component should not know anything about the reading or writing state of an application, network protocols, or non-UI providers farther up the chain. 

    The views should not be aware of the protocol you use to speak to a backend or the format that the data takes. Views should not know about the custom states of your contexts and providers for sharing the state of your application. 

     Both the viewers and controllers have their own state, but the state in views is for the purpose of the UI only. As such things most often belong in controllers, a view should not call hooks useEffect() and useCorntext() except for UI- specific cases like the following: 

     

    1)    Accessing context for UI-specific data and behavior, for example theming and routing. 

    2)    Syncing prop changes with the local state with useEffect()

     

    An example of the code is given below: 

    This code gives us an opportunity to refactor. 

     

    function App() {

      return <EditCustomer id={1} />;

    }

     

    function EditCustomer({ id }) {

      let { customers, dispatch } = useCustomers(); // access context and probably trigger side effects

     

      let customer = customers.find(c => c.id === customerId);

      if (!customer) {

        return <NotFound />;

      }

     

      let [errors, setErrors] = React.useState()

      let [saving, setSaving] = React.useState(false)

      let [name, setName] = React.useState(customer.name);

      let [email, setEmail] = React.useState(customer.email);

     

      let saveCustomer = () => {

        setSaving(true)

        fetch({

          url: `/api/customers/${id}`,

          method: “PUT”,

          headers: { “Content-Type”: “application/json” },

          body: JSON.stringify({ name, email })

        })

          .then(response => response.json())

          .then(apiCustomer => {

            // Formatting for differences between backend and frontend

            //   e.g. Rails/Django snake_case into JavaScript camelCase

            dispatch({

              type: “UPDATE_CUSTOMER”,

              payload: formatChangeForFrontend(apiCustomer)

            });

          })

          .catch(error => {

            setErrors(error)

          });

          .finally(() => {

            setSaving(false)

          })

      };

     

      return (

        <div>

          {errors && <ErrorDisplay errors={errors} />}

          <input

            type=“text”

            name=“name”

            value={name}

            onChange={e => setName(e.target.value)}

          />

          <input

            type=“text”

            name=“email”

            value={email}

            onChange={e => setEmail(e.target.value)}

          />

          <button onClick={saveCustomer} disabled={saving}>Save</button>

        </div>

      );

    }

     

     

    Refactor to Controller-View pattern

     

    Using the following code, we will separate the UI from the domain logic. 

    function App() {

      return (

        <EditCustomerController id={1} />;

      )

    }

     

    // Notice explicit suffix “Controller”

    function EditCustomerController({ id }) {

      let { customers, dispatch } = useCustomers();

     

      let customer = customers.find(c => c.id === id);

      if (!customer) {

        return <NotFound />;

      }

     

      let onSave = (newCustomerData) => {

        return fetch({

          url: `/api/customers/${id}`,

          method: “PUT”,

          headers: { “Content-Type”: “application/json” },

          body: JSON.stringify(newCustomerData)

        })

          .then(response => response.json())

          .then(apiCustomer => {

            // Formatting for differences between backend and frontend

            //   e.g. Rails/Django snake_case into JavaScript camelCase

            dispatch({

              type: “UPDATE_CUSTOMER”,

              payload: formatChangeForFrontend(apiCustomer)

            });

          })

      };

     

      return <CustomerForm onSave={onSave} initialName={customer.name} initialEmail={customer.email} />

    }

     

    // Notice no special name; just a React component that knows about React things

    function CustomerForm({onSave, initialName, initialEmail}) {

      let [errors, setErrors] = React.useState()

      let [saving, setSaving] = React.useState(false)

      let [name, setName] = React.useState(initialName);

      let [email, setEmail] = React.useState(initialEmail);

     

      let onSaveWrapped = () => {

        setSaving(true)

        onSave({name, email})

          .catch((error) => {

            setErrors(error)

          })

          .finally(() => {

            setSaving(false)

          })

      }

     

      return (

        <div>

          {errors && <ErrorDisplay errors={errors} />}

          <input

            type=“text”

            name=“name”

            value={name}

            onChange={e => setName(e.target.value)}

          />

          <input

            type=“text”

            name=“email”

            value={email}

            onChange={e => setEmail(e.target.value)}

          />

          <button onClick={onSaveWrapped} disabled={saving}>Save</button>

        </div>

      );

    }

     

     

    There are many upsides of using this code and a few of them are- 

    1)    The view component <CustomerForm/> is far easier to test than the previous component. It requires no mocking of the network or provider/context setup. The tests of this are called unit tests.

    2)    Any tests that are conducted on the original <EditCustomer/> component will still pass after you update the import/name! These tests are now called integration tests. 

    3)    The controller component helps isolate all the non-UI concerns in the <EditCustomerController/> and passed into the view. All the view knows that it gets its initial values for name and email and async onSave() callback. 

    4)    The coupling between the view and the rest of our app is minimal. This view could be dropped into any other location in your React Tree. 

    5)    The UI concerns around form control, error and saving states is kept inside the view. 

    6)    This same pattern works in TypeScript as well. 

     

    If you do decide to stick with JavaScript as your language, you can even further decouple the controller from the view using the following code: 

     

    function App() {

      return (

        <EditCustomerController id={1}>

          <CustomerForm />

        </EditCustomerController>

      );

    }

     

    function EditCustomerController({ id, children }) {

      // … all the same code

      return React.cloneElement(children, {

        initialName: customer.name,

        initialEmail: customer.email,

        onSave

      });

    }

     

    // … view stays the same

     

    A few benefits of this code are: 

     

    1)    Same testing benefits

    2)    The controller is uncoupled from the view and could be composed with any other view that accepts those props. 

    3)    The composing parent could add additional props to the view that it knows about and the controller may not.

     

    In cases where the <app/> has some bit of additional data that the controller was ignorant of: 

    function App() {

      let importantData = {

    /* things */

      };

      return (

    <EditCustomerController id={1}>

       <CustomerForm importantData={importantData} />

    </EditCustomerController>

      );

    }

     

    // … controller stays the same

     

    function EditCustomerController({ id, children }) {

      // … all the same code

    }

     

    // … view accepts extra prop `importantData`

    function CustomerForm({ onSave, initialName, initialEmail, importantData }) {

      // does things

    }

     

    Pillar 2: UI-Agnostic Data Model

    Now that our controller and view have been separated, let’s look at our controller: 

    let { customers, dispatch } = useCustomers();

     

    let customer = customers.find(c => c.id === id);

    if (!customer) {

      return <NotFound />;

    }

     

    let onSave = newCustomerData => {

      return fetch({

        url: `/api/customers/${id}`,

        method: “PUT”,

        headers: { “Content-Type”: “application/json” },

        body: JSON.stringify(newCustomerData)

      })

        .then(response => response.json())

        .then(apiCustomer => {

          // Formatting for differences between backend and frontend

          //   e.g. Rails/Django snake_case into JavaScript camelCase

          dispatch({

            type: “UPDATE_CUSTOMER”,

            payload: formatChangeForFrontend(apiCustomer)

          });

        });

    };

     

    Our data os accessed via fetch at an HTTP URL and the backend speaks JSON while giving us data with a non-friendly casing and expects the keys “name” and “email” when creating a customer. 

     

    We can fix this function by pulling it out of our component: 

    function EditCustomerController(props) {

      // …

     

      let onSave = async newCustomerData => {

        // NOTE: new line!

        let latestCustomer = await performCustomerUpdate(props.id, newCustomerData);

     

        dispatch({ type: “UPDATE_CUSTOMER”, payload: latestCustomer });

      };

     

      // …

    }

     

    // NOTE: new function!

    async function performCustomerUpdate(id, newCustomerData) {

      let response = await fetch({

        url: `/api/customers/${id}`,

        method: “PUT”,

        headers: { “Content-Type”: “application/json” },

        body: JSON.stringify(newCustomerData)

      });

      let apiCustomer = await response.json();

      return formatChangeForFrontend(apiCustomer);

    }

     

    We pulled this function out because it wasn’t presentation logic. To do something more complicated, use the following code:

     

    function EditCustomerController(props) {

      // …

     

      let onSave = async newCustomerData => {

        // NOTE: yep, that’s a `new` keyword

        let gateway = new CustomerGateway();

        let latestCustomer = await gateway.update(props.id, newCustomerData);

     

        dispatch({ type: “UPDATE_CUSTOMER”, payload: latestCustomer });

      };

     

      // …

    }

     

    // NOTE: A class!!!

    class CustomerGateway {

      constructor(fetchFn = fetch) {

        this.fetch = fetchFn;

      }

     

      async update(id, data) {

        await this.fetch({

          url: `/api/customers/${id}`,

          method: “PUT”,

          headers: { “Content-Type”: “application/json” },

          body: JSON.stringify(data)

        });

     

        let formattedData = this.formatChangeForFrontend(await response.json());

     

        // do more things …

        return formattedData;

      }

     

      formatChangeForFrontend(apiData) {

        // return transform data from snake_case to camelCase

      }

    }

     

    We can also take another step further by using the following code: 

    function EditCustomerController({ Gateway = CustomerGateway }) {

      // …

     

      let onSave = async newCustomerData => {

    let gateway = new CustomerGateway();

    let latestCustomer = await gateway.update(props.id, newCustomerData);

    // …

      };

     

      // …

    }

     

    /* OR */

     

    function EditCustomerController({ updater = performCustomerUpdate }) {

      // …

     

      let onSave = async newCustomerData => {

    let latestCustomer = await updater(props.id, newCustomerData);

    // …

      };

     

      // …

    }

     

    These examples illustrate two things-

     

    1)    Keep moving non-UI behavior and knowledge out of components. 

    2)    Use the modeling technique that you are comfortable with and fit the problems. 

     

    Conclusion- 

     Today, MVC is commonly viewed as a “server-side architecture” that does not map well to GUI programs. This assumption ignores the origins of MVC and the numerous successful implementations. JavaScript is a very general language that favors a mixed style far better than a single one. The pattern also meshes well with the spirit of React as a “just a view library”.

     

    Recent Articles

    React Fragment: Find out what we encountered.!

      React Fragment: Hello Devs, in my journey of learning react I came across some frontend conditions where I had to render multiple react components,...

    React Admin: Documentation | Features | Dahboard | Templates

      React Admin is a framework for the front-end development of data-driven applications. That is operating in a browser over the top of REST or...

    React Hooks

      A hook is a component that allows us to use the features of React and state without defining a class. This feature is added...

    React Animation: A definitive guide

    React Animations Add-ons The ReactTransitionGroup add-on is a lower-level Applications Program Interface (API) component for creating react animations. And the ReactCSSTransitionGroup, is a component used...

    Create React App: A Complete guide for Beginners

      Create React App is the modern way of building an official Single Page React Application. It sets up a comfortable environment for building react...

    Related Stories

    Leave A Reply

    Please enter your comment!
    Please enter your name here