Please clone the project to follow along: https://github.com/amwais/react-hooks-and-context.

The master branch is the final version, and each branch represents a step as this tutorial moves on.

V01: Basic Hooks usage

I’ve used create-react-app to bootstrap the project.

As you can see, I’ve changed App.js to be a functional component, as we no longer need to use class based components.

This file contains the basic working version of the app: A header, an “Add Todo” form containing a text input field and an Add button, and finally the rendered list of Todo items. These are not yet encapsulated to separate component files, just mashed together for now.

Two hooks are being used here: useState and useReducer.

useState, the simpler of the two, is responsible for keeping the local state of the form component:

const [ textField, setTextField ] = useState('');

useState is a hook you can call as many times as needed, and each call is responsible for storing exactly one state key. We use Array De-structuring to get the two returned values from the hook: the name of our state key (textField) and the name of the function that’s responsible for modifying it (setTextField).

Now, in our ‘yet-to-be-a-separate-component’ form, we can easily use this local state to create a controlled input field:

<input autoFocus type="text" value={textField} onChange={(e) => setTextField(e.target.value)} />

Notice how calling stuff like this.state.textfield and this.setState({ text: e.target.value }) is unneeded, and is replaced by two compact and direct calls.

Let’s look at useReducer now. This hook is more complex in comparison to useState, and it is very similar to the Redux way of managing global state.

Let’s start by creating a reducer, outside the scope of our App component:

const appReducer = (state, action) => {

switch (action.type) {

case 'ADD_TODO':

return [...state, {

text: action.payload

}];

case 'REMOVE_TODO':

return state.filter((_, index) => index !== action.payload);

case 'TOGGLE_COMPLETED':

return state.map((item, index) => {

if (action.payload === index) {

return {

...item,

completed: !item.completed

};

}

return item;

});

default:

return state;

}

};

As you can see, no difference in the way a reducer is usually defined when working with Redux.

Now, inside our App component, let’s call useReducer:

const [state, dispatch] = useReducer(appReducer, [{

text: 'Eat Dinner',

completed: false

}]);

This is where the magic happens: No need to use the connect function from Redux and wrap our component with it. Instead, we provide the hook with our reducer, in addition to an initial state, and voila: we get access to the state, and to the dispatch function. Simple. Clean. Simple.

For example, we can now create action functions that will call the dispatch function, targeted at our reducer:

const handleDelete = (index) => {

dispatch({

type: 'REMOVE_TODO',

payload: index

});

};

And call this function from our Delete button:

<button onClick={() => handleDelete(index)}>Delete</button>

V02: Component Encapsulation with Context

The next step in our App will be to separate each of the three components to a file of its own.

This is a common pattern in React, but with Redux involved, it used to be a somewhat tedious task, as one had to create the component, and then wrap this component in a connect function, using mapStateToProps (Used to map state keys to the component’s props) and mapDispatchToProps (Used to map action functions with dispatch to the component’s props).

But now, without Redux, how could we call dispatch or access the reducer’s (global) state from inside the encapsulated component?

This is where the Context API comes into play and makes developing React applications a more enjoyable experience. First, look at App.js in this version: Now it contains the returned JSX, our hooks definition and our reducer definition (which will also be separated next). In addition, a new context is created:

export const TodosDispatch = React.createContext(null);

This TodosDispatch context is used in order to provide any child components under it, an access to the reducer’s dispatch function. We provide it like this:

<div className = "App" >

<TodosDispatch.Provider value = {dispatch} >

<Header / >

<AddToDoForm / >

<ToDoList todos = {state}/>

</TodosDispatch.Provider >

</div>

Now, let’s look at AddToDoForm.js as an example for a component that requires access to this context. First, we import the context:

import { TodosDispatch } from '../App';

Next, we call the useContext hook:

const dispatch = useContext(TodosDispatch);

And that’s it, we have access to the App “store”, and we can call the dispatch function from our child component:

const handleAddToDo = (e) => {

e.preventDefault();

if (textField.length > 0) {

dispatch({

type: 'ADD_TODO',

payload: textField

});

setTextField('');

}

};

V03: Using multiple reducers

But what if we have multiple reducers, and some components requires access to them? Maintaining the same pattern should get us what we want. In the branch, I’ve added an error reducer, that will be used as a global way to display errors in different components. We start by creating another context:

export const ErrorsContext = React.createContext(null);

Next, we create the reducer:

const [errors, errorDispatch] = useReducer(errorReducer, {

header: '',

form: ''

};);

Now, in our components tree, we wrap the components that requires access to this context:

<div className = "App" >

<TodosDispatch.Provider value = {todosDispatch} >

<ErrorsContext.Provider value = {[errors, errorDispatch]}>

<Header />

<AddToDoForm />

</ErrorsContext.Provider>

<ToDoList todos = {todos}/>

<ErrorsContext.Provider value = {errorDispatch}>

<ErrorButtons />

</ErrorsContext.Provider>

</TodosDispatch.Provider>

</div>

Notice how we can use multiple contexts, and we can wrap any component we want with any context.

Also notice, that the value we are attaching to this context doesn’t have to be just the dispatch function, but can also be an array:

<ErrorsContext.Provider value = {[errors, errorDispatch]}>

This is how the Header and AddToDoForm are able to access the actual error reducer state.

Further reading:

https://reactjs.org/docs/hooks-reference.html

https://reactjs.org/docs/context.html