April 6, 2019

Moving from Redux to React Hooks Part 1

Moving from Redux to React Hooks Part 1

For the majority of my React projects I choose redux for state management. I like to separate my reducers and action creators and then pass them on the the store creator of redux together with the redux-thunk middleware to dispatch asynchronously my actions within my fetch promise chain.

With React 16.8 came hooks and with hooks came a new era of React state management. Redux is not necessary anymore when using hooks in combination with the React context api.

Let's first take a look at what the new hook plus context version has in common with the way we done things with redux. First of all, you still got to inject your state container or store into the component tree, so child components can access the context. Similar to redux when we had to wrap the component in a <Provider> component and pass on the store as prop.

<Provider store={store}>
    <Container>
        <ChildComponents />
    </Container>
</Provider>

Also something that both have in common is the reducer. In the example below you see a reducer we would pass into the useReducer hook.

const getComments = (state, action) => {
    switch(action.type) {
        case "GET_COMMENTS_REQUEST":
            return {...state, isLoading: true};
        case "GET_COMMENTS_SUCCESS":
            return {...state, result: action.payload, isLoading: false};
        case "GET_COMMENTS_FAILURE":
            return {...state, isLoading: false, error: action.error}
        default:
            throw new Error();
    }
}

As you can see in the example getComments reducer we don't assign an initial state object to the state. Instead you pass your initial state into the hook itself.

[state, dispatch] = useReducer(reducer, initialState);

Once we destructed the return of useReducer we can use the dispatch function as usual to call our action.

<Button onClick={() => dispatch(fetchComments())}>Load Comments</Button>

The state we got out of our hook can be used as usual to access the state of our store. To check if our state has changed we can use the useEffects hook, it takes a function that gets called after the component has been rendered and if it has been updated. As second argument you can pass in the state you want to "observe", so once the state hasn't changed it stops the function from re-running. There's also the difference between our regular component lifecycle methods and the hook. With the use of this hook we need to add some condition to stop the function from calling itself.

Down below is a small example of using the hook.

useEffect(() => {
    // do something...
}, state.comments)

That's basically it you have to know to manage your state together with React hooks and the context api. We start now with an example that demonstrates the usage a little bit more practical. To show the state change a bit more visual we use Mapbox in our small example application. If you don't know much about Mapbox, you can get more information here but it is not required to know the framework itself, since we use the React wrapper of it which behaves as a usual component.

I assume you know how to set up a new project and how to use webpack, babel and so on. Specifically for this project you need the Mapbox binding for React as well as the original package, to install it run:

npm install react-mapbox-gl mapbox-gl --save

After installing the dependencies, let's create our map component:

import React from "react";
import ReactMapboxGl from "react-mapbox-gl";

const MapInstance = ReactMapboxGl({
  accessToken: "your-token"
});

const Map = () => {
  return (
    <div>
      <button onClick={()=>{}}>
        +
      </button>
      <button onClick={()=>{}}>
        -
      </button>
      <MapInstance
        style="mapbox://styles/mapbox/streets-v9"
        center={[0,0]}
        containerStyle={{
          width: "100vw",
          height: "100vh"
        }}
        zoom={[12]}
      />
    </div>
  );
};

export default Map;

We want to keep our store, actions and reducers outside of our components, so let's add first a folder named store in your src directory or wherever your application files are located. Within the folder we add an index.js file where we will build our store and export the provider and the context.

import React, { useReducer } from "react";

export const Context = React.createContext();

export const ContextProvider = ({ ...props }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return <Context.Provider value={{state, dispatch}}>{props.children}</Context.Provider>;
};

We have now one major problem with the way we build our store. We want one store that holds all our reducers and we can inject to our provider instead of having now multiple providers with multiple stores. To realize this we have to change some things. If you have used Redux before that you are probably familiar with the createStore function as well as the combineReducer helper function that Redux kindly offers. Because it worked well like that we will try to reproduce this kind of behavior. First of all we need to restructure our store a little.

import React from "react";
import { createStore } from "./createStore";
import { someReducer, anotherReducer } from "./reducers";

export const Context = React.createContext();

const rootReducer = {
  someReducer
  anotherReducer
};

export const ContextProvider = ({ ...props }) => {
  const store = createStore(rootReducer);

  return <Context.Provider value={store}>{props.children}</Context.Provider>;
};

As you can see we now import a createStore function and some reducers we will create soon. Instead of destructing the useReducer hook and using the state and the dispatch function directly we call now our createStore function and pass in our rootReducer object which holds our reducers. This will return another object which holds all our states and dispatch functions. Next, we pass the store into the value prop of the context provider component.

Part 2

>>> Check out my Udemy courses.