Wednesday, October 16, 2019

useContext

useState hook helps us persist information locally in a component. What if we want to use data across components. Indeed there's a complicated way in the form of useState. We can declare useState at a top level component and then use props to pass those components to child components and manipulate global state.

Sure that does work and I have done that myself. If your application is nothing more than 2 pages and has zero complexity, such a solution works. You don;t want to deal with the complications of MobX or Redux, local state should be good enough. But seldom we deal with such applications. A typical application may have user data, preferences, API information to be used across the application. We could use a variety of strategy to put together a working solution. We could have API information as a part of ENV, user details in things like cookie, and finally data in local storage (PWA approach). But you see how the application is broken, with very little strategy. If we wish to go local storage, would it not be easier if we have some global context and we persist that global context as opposed to objects from different classes and components?

In comes useContext. React Hooks most liked item is "useContext". People who dealt with the complexity of Redux for smaller application can talk days about how useContext changed their approach and ease of application. Conversely, with context and useReducer hook many started to understand how Redux actually works and started to use Redux judiciously as opposed to walk in opposite direction.

Let's start by building a simple context and then progress into something more concrete.

A single Provider Context

There are 3 sides to using context

  1. Create a store
  2. Add provider
  3. Add consumer
Let's start by building a very simple context app that will remember something as simple as a a number that we can access across components, without passed as props.

To start with we create store for context, say numcontext.js

Code for store
import { createContext } from "react";

const NumContext = createContext();
export default NumContext;

Provider code
With the context created, we need add it to our index.js, along with initial value of kind

<NumContext.Provider value={42}>
 <App />
</NumContext.Provider>

Consumer code
For us to consume this context, we need to add consumer attribute on NumContext in our component and that's it!
import React from "react";
import NumContext from "./numcontext";

function App() {
  return (
    <div className="container">
      <h2>A simple Number context!</h2>
      <NumContext.Consumer>
        {(value) => (<p>Some text - {value}</p>)}
      </NumContext.Consumer>
    </div>
  );
}

export default App;

Now, go ahead, create another component in your application and add it to App.js or in index.js (Within Provider tag). Do the same as above. Create a consumer tag and a callback function to read from context.

Above example is good for us to start. For all practical reasons we need to a more realistic example. Now let's consider a simple logon application. We create context first, empcontext.js

import {createContext} from 'react';

const EmpInit = {
  firstName: 'Tom',
  lastName: 'Moody',
  email: 'tom.moody@dontopen.com',
  updateDetails: function(details) {
    this.firstName = details.firstName;
    this.lastName = details.lastName;
    this.email = details.email;
  }
};

const EmpContext = createContext();

export {EmpContext, EmpInit};

Next step is to include this context into index.js/App.js or your parent component

<EmpContext.Provider value={EmpInit}>
    <EmpComponent />
    <Login />
</EmpContext.Provider>
Note: Don't forget to add your imports. That's important :)

Finally add consumer in your component to consume data, like the following, say empcomponent.js

import React from "react";
import { EmpContext } from "./empcontext";

const EmpComponent = () => {
  return (
    <div className="container">
      <h4>Employee details</h4>
      <EmpContext.Consumer>
        {data => {
          return (
            <dl className="row">
              <dt className="col-sm-3">Name</dt>
              <dd className="col-sm-9">
                {data.firstName} {data.lastName}
              </dd>
              <dt className="col-sm-3">Email</dt>
              <dd className="col-sm-9">
                {data.email}
              </dd>
            </dl>
          );
        }}
      </EmpContext.Consumer>
    </div>
  );
};

export default EmpComponent;

Whenever EmpComponent is displayed, you should see it glimmer with your init values. Keep changing the init values to see the display change. Now wait, I know what you are thinking. You say there's more to the story. Let's add a login form to update context, then see, if updated state shows up.

Note: Add a simple page router and have different pages for your application. Keep it simple for experimentation sake


import React, { useState } from "react";
import { EmpContext } from "./empcontext";
const Login = () => {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [email, setEmail] = useState("");

  const onUpdateDetails = context => {
    context.updateDetails({firstName, lastName, email});
  };
  return (
    <div className="container">
    <EmpContext.Consumer>
      {(context) => {
        console.log(context);
        return (
          <form>
            <div className="form-group">
              <input
                type="text"
                className="form-control"
                id="firstName"
                placeholder="First Name"
                value={firstName}
                onChange={e => setFirstName(e.target.value)}
              />
            </div>
            <div className="form-group">
              <input
                type="text"
                className="form-control"
                id="lastName"
                placeholder="Last Name"
                value={lastName}
                onChange={e => setLastName(e.target.value)}
              />
            </div>
            <div className="form-group">
              <input
                type="email"
                className="form-control"
                id="email"
                placeholder="Email ID"
                value={email}
                onChange={e => setEmail(e.target.value)}
              />
            </div>
            <button className="btn btn-info" onClick={(e) => { 
              e.preventDefault();
              onUpdateDetails(context);
            }}>
              Update
            </button>
          </form>
        );
      }}
      </EmpContext.Consumer>
    </div>
  );
};

export default Login;

With router in place, go ahead implement Login component for say /login route and /details component for EmpComponent. Play around and see details change as you change value's in /login route.

Multi-Provider Context


Application rarely has one context. Indeed you'll want an over arching AppContext, but then different modules will need respective context - Separation Of Concerns (SOC). With context hooks, its much, much easier than you think. Here's a sample of what I mean.

<div>
  <AppContext.Provider value={appInit}>
    <Navbar />
    <NumContext.Provider value={42}>
      <App />
    </NumContext.Provider>
    <EmpContext.Provider value={EmpInit}>
      <EmpComponent />
      <Login />
    </EmpContext.Provider>
  </AppContext.Provider>
</div>

What's Next?

We saw useState earlier. useState helps to maintain state for simple components. As we strive to have stateless components, useState starts to wither and give way to context. With context we still have a lot of nut and bolts to take care of. That's were useReducer hook comes to our rescue. Let's look at that next. useReducer is a very powerful concept and simple to handle. You'll see how it'll change the way we handle state. Down the line when we do useRedux, you'll see how easy it is to follow as opposed to the time you spent learning Redux the old school way.

Is this a replacement for Redux?


Context works great for simple cases and to an extent for certain complex cases too. But saying context can replace Redux is taking it too far from my little experience. I'd give rather a subjective answer to this "It depends". I'm a firm believer on "Do what's needed". Do start an application with Redux. Start small, see if the application performs with what's available in the first place. Slowly expand your contours, if the need is not fullfilled with context, go for Redux. 

Do you need to change to Class based development for Redux?
No. Redux supports hooks. You'll see a blog post coming soon on how to add Redux to hooks.

No comments: