Yes, I know what you're thinking. This isn't just another todo list tutorial, is it?
In this post, I'll be talking about a little advanced concept called React Context and how to use it in an application by building a very basic todo list app.
Expectations
This post assumes a basic understanding of the react framework.
Let's get started
What is Context?
According to official react documentation
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
Why/When do we need to use Context?
React documentation to the rescue again
In a typical React application, data is passed top-down (parent to child) via props, but such usage can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.
Okay, enough with the introduction. Now we build a basic react app using create-react-app starter kit.
For those who would like to see the finished app, here is a preview of the complete code
First create a Todo.js
file inside the src
directory and add the following code to it. This component will render a button and a controlled input
import React, { useState } from "react";
export default function Todo() {
const [todo, setTodo] = useState("");
return (
<form>
<input
type="text"
name="todo"
value={todo}
onChange={(e) => setTodo(e.target.value)}
/>
<button>Add todo </button>
</form>
);
}
Next create a TodoList.js
file and the src
directory. This component will render our todo list, but for now it simply renders a h1
heading for the list.
import React from "react";
export default function TodoList() {
return (
<div>
<h1> Todo List </h1>
</div>
);
}
Now we are getting to the interesting part.
To use Context in a react app, we need to create the context. So create a TodoContext.js
in the same src
directory.
Add the following line of code to it. Yes, I know we don't have a ./reducer.js
file yet. Don't worry, we will create it soon.
import React, { createContext, useReducer } from "react";
import reducer, { initialState } from "./reducer";
Create a context named todoContext
by adding the next line of code
export let todoContext = createContext({});
Next create function component Provider
and pass down children
as props to it.
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
const values = {
state,
dispatch
};
return (
<>
<todoContext.Provider value={values}>
{children}
</todoContext.Provider>
</>
);
}
let's explain the code above.
- useReducer - The useReducer Hook is similar to the useState Hook. It allows for custom state logic. If you find yourself keeping track of multiple pieces of state that rely on complex logic, useReducer may be useful. This hook accepts two arguments, a reducer and an initial state.
- Our
Provider
component returns a context.Provider component which accepts a value prop and passes it down to the children of ourProvider
component.
Here is the full code for the TodoContext.js
import React, { createContext, useReducer } from "react";
import reducer, { initialState } from "./reducer";
export let todoContext = createContext({});
export default function Provider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
const values = {
state,
dispatch
};
return (
<>
<todoContext.Provider value={values}>
{children}
</todoContext.Provider>
</>
);
}
Let's create our reducer.js
file and add the code below to it
import { ADD_TODO } from "./constants";
export const initialState = {
todos: []
};
const reducer = (state, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos, action.payload]
};
default:
return state;
}
};
export default reducer;
The above code creates an initial state and initializes todos to an empty array. We then added a reducer function, a reducer function accepts a state and action as arguments and returns the updated state based on the type of action dispatched, which in our case we have an action called ADD_TODO that will be defined in constants.js
.
Create the file constants.js
and add following code to it
export const ADD_TODO = "ADD TODO";
As we can see, constants.js simply creates and exports a string variable.
Next we update the code inside our app.js
file by importing the TodoContext.js
, Todo.js
and TodoList.js
component.
app.js
now should look like this
import React, { Component } from "react";
import Todo from "./Todo";
import TodoList from "./TodoList";
import Provider from "./TodoContext";
export default class App extends Component {
render() {
return (
<div className="App">
<Provider>
<Todo />
<TodoList />
</Provider>
</div>
);
}
}
Since we have wrapped our components in a Provider component, we can now be sure that we have access to the context in our Todo.js
and TodoList.js
.
We can update out Todo.js
file to now access the context value and also create a function addTodoHandler
that dispatches the ADD_TODO action type.
Below is the complete code for Todo.js
import React, { useState, useContext } from "react";
import { todoContext } from "./TodoContext";
import { ADD_TODO } from "./constants";
export default function Todo() {
const [todo, setTodo] = useState("");
const { dispatch } = useContext(todoContext);
const addTodoHandler = (e) => {
e.preventDefault();
dispatch({ type: ADD_TODO, payload: todo });
setTodo("");
};
return (
<form>
<input
type="text"
name="todo"
value={todo}
onChange={(e) => setTodo(e.target.value)}
/>
<button onClick={addTodoHandler}>Add todo </button>
</form>
);
}
Cool. Now all we need to do is display our Todo whenever a user clicks the Add todo button. And we do this by mapping over our todos array defined in the reducer.js and returning the jsx.
Below is the complete code for the TodoList.js
import React, { useContext } from "react";
import { todoContext } from "./TodoContext";
export default function TodoList() {
const { state } = useContext(todoContext);
return (
<div>
<h1> Todo List </h1>
{state.todos.length > 0 &&
state.todos.map((todo, id) => {
return <h4 key={id}>{todo}</h4>;
})}
</div>
);
}
That's it. Now you have learnt what context means, when to use it and what a reducer is and most importantly, you've learned how to implement them in an application, albeit a small one :).
Disclaimer: To build a todo app like this one, using context and reducer is kind of overkill is unnecessary. But to better demonstrate and explain these concepts, I had to choose an example that won't be too complex to understand.
Thank you for reading, you can reach out to me for suggestions or corrections.
Top comments (0)