Saturday, October 19, 2019

Use Reducer - 2

In our earlier article we saw how to use both a simple and an object based state with reducer. Reducer's are powerful functions and their purpose is revealed as app/component complexity increases. The more we try to localize reducer functionality in to a single component, the more we build heavier components and lesser are our chance to re-use components.

It's no longer about a component consuming a simple reducer. We'll have scenarios where our components may need multiple reducers. By moving reducer to upper layers is the best way for us to handle a complete solution. Say for e.g. a simple login form. As soon as a user logs in, there'll be sections of application wanting to know a unique ID of user, other sections needs name of user, while others just need to know if the user is logged in or not.




Our approach for this solution is to start with a reducer, associate with context, allow context to work with components.


Although above is a Redux flow chart, our approach for reducer + context is pretty much the same. Now you know why confusions arise on will Reducer + Context replace REDUX.

export const initialState = {
  data: 'NO_DATA',
  userinfo: {
    email: '',
    username: 'GUEST'
  }
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_DATA':
      return {
        ...state,
        data: action.payload
      }
    case 'LOGOUT':
    case 'RESTORE_STATE':
      return initialState;
    case 'LOGIN':
      return {
        ...state,
        userinfo: action.payload.userinfo
      }

    default:
      return state;
  }
}

export default reducer;

Above code is a simple reducer that has an initial state. In a real world situation, FETCH_DATA will make a call to remote service. But not here. Let's keep it simple. It's not long before we build & invoke a remote service from reducer. But not now. "FOCUS". In addition, we have a simple LOGIN and LOGOUT handler update and clean state.

Next step, create a store and context 

import React, {createContext, useReducer} from 'react';
import reducer, {initialState} from "../reducers";

const Store = createContext();


const Provider = ({children}) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const store = {state, dispatch};
  return (
    <Store.Provider value={store}>
      {children}
    </Store.Provider>
  )
}

export {Store, Provider}

We now have a custom component "Provider" that injects Store context. Any child of Provider will now have a context and in the process reducer. Context as we saw from previous examples can be nested too. If you want to define any additional context to components down the line this code does not hinder you in anyway. Our next step is to integrate our App component

import React from 'react';
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'
import HomePage from "./components/pages/home";
import {Provider} from "./components/store";
import Appbar from "./components/controls/Appbar";
import Features from "./components/pages/features";

function App() {
  return (
    <div>
      <Router>
        <Provider>
        <Appbar/>
        <div className="container">
        <h2>Let's try a Reducer on global context!</h2>

          <Route exact path="/" component={HomePage}/>
          <Route path="/features" component={Features}/>

        </div>
        </Provider>
      </Router>
    </div>
  );
}

export default App;

Our App component hosts other component's. For brevity, let me explain you, why the structure and some reason for this madness


We have a multiple pages and pages with multiple components. Objective is to see how we can exchange data from one component to another. Across pages, components within pages, with refresh and without refresh. When we move from one page to another, we go through page lifecycle, so refresh happens automatically, but Navbar is constant across the application, to refresh data automatically is some achievement. Even within the same page, data from one component to another should be shared without props and other events. Lets's start with Home page. It has 2 components - Component1 & Component 2.

HomePage.js
============
import React from 'react';
import Component2 from "../controls/component2";
import Component1 from "../controls/component1";

const HomePage = () => {
  return (
    <div>
      <h4>This is Home page</h4>
      <Component1/>
      <hr />
      <Component2/>
    </div>
  );
};

export default HomePage;

Component1.js
==============
import React, {useContext} from 'react';
import {Store} from "../store";

const Component1 = () => {
  const {state, dispatch} = useContext(Store);
  const onSetContext = (e) => {
    e.preventDefault();
    dispatch({type: 'FETCH_DATA', payload: 'Hello World!!!'});
  }
  const onResetContext = (e) => {
    e.preventDefault();
    dispatch({type: 'RESTORE_STATE'});
  }
  return (
    <div>
      <button className="btn btn-primary" style={{margin: '15px'}} onClick={onSetContext}>Set Context</button>
      <button className="btn btn-secondary" style={{margin: '15px'}} onClick={onResetContext}>RESET Context</button>
    </div>
  );
};

export default Component1;

Component2.js
==============
import React, {useContext} from 'react';
import {Store} from "../store";

const Component2 = () => {
  const {state, dispatch} = useContext(Store);
  return (
    <div className="container">
      <p className="success"> Value in state is <small>{state.data}</small></p>
    </div>
  );
};

export default Component2;

Component1updates state and Component2 shows current content from state. Now come's a question - can 'useState' handle this better? Answer is YES. But wait for complete answer down below and my reasons to go with reducer + context. This code is like any other component connected to a 'useReducer' hook (but done through useContext, silly). You have state & dispatch mechanism. When we want to observe data, you fetch from state and when you wish to update state, you dispatch your action through 'dispatch' object. Nothing different or new from what we saw in earlier article.


How about, fetching these values from another page. That'll not be any different. You add 'useRedcuer' hook to this new page, get contents of state and use it. As simple as that.

Now, let's take a more objective example of login/registration. A simple features page, has a form and it is subscribed to app context through useContext

import React, {useContext, useState} from 'react';
import {Store} from "../store";

const Features = () => {
  const [username, setUsername] = useState();
  const [email, setEmail] = useState();
  const {state, dispatch} = useContext(Store);
  const onLogin = (e) => {
    e.preventDefault();
    dispatch({type: 'LOGIN', payload: {userinfo: {email, username}}});
  }
  return (
    <div>
      <form>
        <div className="form-group">
          <label htmlFor="exampleInputEmail1">Email address</label>
          <input type="email" className="form-control" id="exampleInputEmail1" aria-describedby="emailHelp"
                 placeholder="Enter email" onChange={(e) => setEmail(e.target.value)} />
            <small id="emailHelp" className="form-text text-muted">We'll never share your email with anyone
              else.</small>
        </div>
        <div className="form-group">
          <label htmlFor="exampleInputName">Name</label>
          <input type="text" className="form-control" id="exampleInputName"
                 placeholder="Full Name" onChange={(e) => setUsername(e.target.value)}/>
        </div>
        <button type="button" className="btn btn-primary" onClick={onLogin}>Submit</button>
      </form>
    </div>
  );
};

export default Features;

Agin, code is similar to above samples. we have a form, update local state with name and email (on hindsight, we could have done a useRef, in place of useState too. That'd have given a lot more freedom to handle button state and things like that. For now, it does not hurt). When user taps on login, we submit the page with dispatch action.. It in turn invokes reducer, updates local state with user information. We don't have anything drastic in our application to show the world post login, but what's important is, you'll see navbar change from 'GUEST' to the username entered. 

import React, {useContext} from 'react';
import {Link} from 'react-router-dom'
import {Store} from "../store";

const Appbar = () => {
  const {state, dispatch} = useContext(Store);
  return (
    <div>
      <nav className="navbar navbar-expand-lg navbar-light bg-light">
        <Link className="navbar-brand" to="/">Navbar w/ text</Link>
        <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText"
                aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
          <span className="navbar-toggler-icon"></span>
        </button>
        <div className="collapse navbar-collapse" id="navbarText">
          <ul className="navbar-nav mr-auto">
            <li className="nav-item active">
              <Link className="nav-link" href="#">Home <span className="sr-only">(current)</span></Link>
            </li>
            <li className="nav-item">
              <Link className="nav-link" to="/features">Features</Link>
            </li>
            <li className="nav-item">
              <Link className="nav-link" href="#">Pricing</Link>
            </li>
          </ul>
          <span className="navbar-text">
      You are a {state.userinfo.username} user!
    </span>
        </div>
      </nav>
    </div>
  );
};

export default Appbar;

Nothing fancy with Appbar. We are updating data from state and the way we listen to state is through 'useContext'.

It'll be an interesting exercise for you to try the same application without reducer with just context alone. Try it out and you are in for some surprise!

useState vs useReducer

We can do a lot of stuff that we do with useReducer in useState. I prefer to go 'useReducer'
  1. Even within component, when we have more than couple of 'state' objects to deal with
  2. When there's business logic or validation to deal with before state is updated
  3. Have a component re-use for the same logic across other components
  4. Better testability
If you have any more thoughts, please do share.

Our next work will be to integrate Redux with React Hooks and capability to associate multiple reducers for context.


No comments: