How To Build An App With React Context API
Elijah Trillionz
Posted on May 30, 2022
Overtime, props have proven to be very useful in passing data across components. But as the application grows, it is almost always certain that most components deep in the tree will require data from parent/top in the tree components. As such, using props will make the whole app cumbersome. For example,
<App>
<Header postsLength={posts.length} /> {/* assuming the header requires the length of the post */}
<PostsList posts={posts}>
<PostItem post={post}>
If the PostItem
should require any new data from the parent component, you'd have to pass the data as props to every single component in-between. This is the reason for state management in React, and React provides a built-in state management solution - React context to handle passing data through descendant components without having to pass props through every component level.
So this is what we will discuss in this article by creating this Blog. I recommend you code along, so at the end of this article, you'd have created a simple blog with React Context API.
Getting Started
Let's set up all we would need to get the app started, and that would be creating a new React app. So head up to your terminal and run the following command
npx create-react-app devblog
When it is done, you can run npm start
to preview the app. We don't need extra dependencies to use React context, so let's get started.
The blog is going to be a very simple one, with only a page to read, add, and delete posts.
Creating components
First off, I want us to create the components we need for this app, so head up to the src directory and create a new directory called components.
1. The header
We will create a simple component for the header of our blog. The header will contain a title and button to create new posts. This button is going to trigger another component which we will attach to the header. So go ahead and create a Header.jsx file and paste the code below
import { useState } from 'react';
import AddPost from './AddPost';
const Header = () => {
const [openModal, setOpenModal] = useState(false);
const closeModal = () => {
setOpenModal(false);
};
return (
<header>
<h1>DevBlog</h1>
<button onClick={() => setOpenModal(!openModal)}>Create Post</button>
{openModal && <AddPost closeModal={closeModal} />}
</header>
);
};
export default Header;
Now we will create the AddPost
component
2. The form component
The form will contain two input fields i.e the title and the body, and a submit button. On submission, we would validate the input to make sure there are values for the title and body. So create an AddPost.jsx file in the components dir.
import { useState } from 'react';
const AddPost = ({ closeModal }) => {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [error, setError] = useState(false);
const validateInputs = (e) => {
e.preventDefault();
if (!title || !body) return setError('All fields are required');
console.log({ title, body });
closeModal();
};
return (
<>
<form onSubmit={validateInputs}>
<input
type='text'
placeholder='Enter title'
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<br />
<textarea
placeholder='Enter body'
onChange={(e) => setBody(e.target.value)}
></textarea>
<br />
<br />
<button type='submit'>Submit</button>
<br />
{error && <p>{error}</p>}
</form>
</>
);
};
export default AddPost;
For now, we are merely logging the values of title and body in the console, but we would do more with it soon.
3. The posts list component
This component is where we loop through the available posts. Each post will trigger a component (the same component) that will read the title and body of the post. So create a PostList.jsx file and past the following
import PostItem from './PostItem';
const PostList = () => {
const posts = [{ id: 1, title: 'a title', body: 'a body' }];
return (
<ul>
{posts.map((post) => (
<PostItem key={post.id} post={post} />
))}
</ul>
);
};
export default PostList;
The posts
array is just a template to preview the UI, we will change it in a bit. But for now, we will create the PostItem
component
4. The post item component
The post item component will use the post
props passed into it to read the title and body of the post. We would also have a delete and an edit button side by side. As we mentioned before only the delete button will be used in this article, but I believe by the time we are done you will know all you need to work on the edit function and make it possible to edit posts.
So go ahead and create a PostItem.jsx file and paste the code below
const PostItem = ({ post: { title, id, body } }) => {
return (
<li>
<h2>{title}</h2>
<p>{body}</p>
<div>
<i className='fas fa-edit'></i>
<i className='fas fa-trash'></i>
</div>
</li>
);
};
export default PostItem;
Right now nothing is being done to the delete button. And by the way, the delete and edit button is represented by a FontAwesome icon, to make fontawesome work in your React app, add the link
tag below to your index.html file in the public directory
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.1/css/all.min.css"
integrity="sha512-gMjQeDaELJ0ryCI+FtItusU9MkAifCZcGq789FrzkiM49D8lbDhoaUaIX4ASU187wofMNlgBJ4ckbrXM9sE6Pg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
Since we are already here (index.html), let's quickly update the title of our app to "DevBlog". In the head
element there is a title
element, change the value from "React App" to "DevBlog". Additionally, you can remove those comments to make it clean, and change the meta
description value to "Blog for devs" or something else.
All these components we've created will have no effect until we include them in our App.js file. So paste the following to replace the current code in App.js
import './App.css';
import Header from './components/Header';
import PostList from './components/PostList';
function App() {
return (
<div>
<Header />
<main>
<h3>
New Posts: <span>1 posts</span> {/*hard-coded, will change later*/}
</h3>
<PostList />
</main>
</div>
);
}
export default App;
You might be wondering why I had to create a PostList
component rather than just looping through the posts in App.js. Well, I can't give a straight answer now but what I can tell you is that the PostList
component is going to be useful in learning context api. And we will also have a footer that will not be a component and that would also be useful in learning a few things about context api. So just hold on to that for now, I promise to explain better in a bit.
Before you save and test run, let's update the CSS
Creating styles
This blog is going to have a dark and a light theme, so we would create an additional CSS file but for now, just copy the following styles into App.css
header {
display: flex;
justify-content: space-around;
align-items: center;
background-color: khaki;
margin-bottom: 40px;
}
button {
padding: 15px;
min-width: 150px;
border: 2px solid rosybrown;
border-radius: 4px;
cursor: pointer;
}
main {
padding: 0 30px;
max-width: 800px;
margin: 0 auto;
}
main h3 span {
font-weight: 400;
font-style: italic;
color: #777;
}
main ul {
list-style: none;
margin: 40px 0;
padding: 0;
}
main li {
border: 2px solid #ccc;
padding: 15px;
border-radius: 6px;
margin-bottom: 30px;
transition: border-color 0.2s ease-in-out;
}
main li:hover {
border-color: #444;
}
main li h2 {
margin-top: 0;
}
main li div {
display: flex;
justify-content: space-between;
align-items: center;
margin-right: 20px;
}
main li i {
cursor: pointer;
}
.fa-trash {
color: tomato;
}
form {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
border: 2px solid #ccc;
padding: 30px;
border-radius: 6px;
}
form input,
form textarea {
width: 300px;
padding: 14px 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
footer {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #fff;
border: 2px solid #444;
border-radius: 50%;
height: 35px;
width: 35px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
Now create an App.dark.css file and add these styles to it
header.dark {
background-color: #444;
color: khaki;
}
header.dark form {
background-color: #444;
}
main.dark li {
border-color: #444;
background-color: #444;
color: khaki;
}
main.dark li:hover {
border-color: #ccc;
}
footer.dark {
background-color: #444;
color: #fff;
}
Go ahead and import the App.dark.css into App.js and save. The dark theme is not going to work just yet because we haven't configured it yet, but we will do that in a bit.
Creating contexts
In the src directory, create a context directory and in it an AppState.js file. We will create our app's context here.
The code examples demonstrated in this section are to explain React context, the actual examples that involve our app will come later on.
How to Create Contexts
React.createContext is a React context API for creating contexts, it is a function that takes a default value as its only argument. This value could represent the initial states of the context as you can access this value wherever you use the context's provider (details coming shortly).
const posts = [];
const AppContext = React.createContext({ posts });
The createContext
function returns a context object which provides a Provider
and Consumer
component. The Provider
component is used to wrap components that will use our created context. Any component that needs access to the values passed as default value (initial state) to the createContext
, will have to be wrapped with the returned Provider
component like this
const App = () => {
return (
<AppContext.Provider>
<Header />
<Main />
</AppContext.Provider>
);
};
The Header
and Main
components can all access the posts
array we set initially while creating the AppContext
context. The provider component provides a value
prop which overrides the default value in AppContext
. When we assign this prop a new value, the Header
and Main
components will no longer have access to the posts
array. But there is a simple approach in which the values provided in the provider component can coexist with the default value of AppContext
, and that is with React states(discussed below).
How to Access Context Values
Components that access and use the values of AppContext
are called consumers. React provides two methods of accessing context values
1. The Consumer Component
I already mentioned this above as it is a component from the AppContext
object just like AppContext.Provider
:
const Header = () => {
return (
<AppContext.Consumer>
{(value) => {
<h2>Posts Length: {value.posts.length} </h2>;
}}
</AppContext.Consumer>
);
};
A request: We have put together everything from where we created the context to now. Kindly do not mind that I am not repeating each step i.e creating the context, providing the context, and consuming the context into one code, but I trust you get it like this.
So inside the consumer component, we created a function that has a value parameter (provided by React), this value is the current AppContext
value (the initial state).
Recall we said the current context value could also be provided by the provider component, and as such it will override the default value, so let's see that in action
const posts = [];
const AppContext = React.createContext({ posts });
const App = () => {
const updatedPosts = [
{ id: 1, title: 'a title', body: 'a body' },
{ id: 2, title: 'a title 2', body: 'a body 2' },
]
return (
<AppContext.Provider value={{posts: updatedPosts}}>
<AppContext.Consumer>
{({posts}) => {
<p>{posts.length}</p> {/* 2 */}
}}
</AppContext.Consumer>
</AppContext.Provider>
)
}
Well, congratulations, you now know how to create and access a context, but there is more you need to know. First, let's take a look at the other method (the most used) to access a context value.
2. The useContext hook
It is a React hook that simply accepts a context object and returns the current context value as provided by the context default value or the nearest context provider.
// from
const Header = () => {
return (
<AppContext.Consumer>
{(value) => {
<h2>Posts Length: {value.posts.length} </h2>;
}}
</AppContext.Consumer>
);
};
// to
const Header = () => {
const value = useContext(AppContext);
return <h2>Posts Length: {value.posts.length} </h2>;
};
Note: Any component that uses the provider component cannot use the useContext hook and expect the value provided by the provider component, i.e
const posts = [];
const AppContext = React.createContext({
posts,
});
const App = () => {
const updatedPosts = [
{ id: 1, title: 'a title', body: 'a body' },
{ id: 2, title: 'a title 2', body: 'a body 2' },
];
const { posts } = useContext(AppContext);
return (
<AppContext.Provider value={{ posts: updatedPosts }}>
<p>{posts.length}</p> {/* 0 */}
</AppContext.Provider>
);
};
This is obviously because the useContext
hook does not have access to the values provided by the provider component. If you need access to it, then it has to be at least one level below the provider component.
But that's not to worry because you would hardly have the need to access it immediately as your app will always be componentized (full of components I mean).
You might wonder, why not use the default posts
as the updated posts
and still get access to it i.e
const posts = [
{ id: 1, title: 'a title', body: 'a body' },
{ id: 2, title: 'a title 2', body: 'a body 2' },
]; // the used to be updatedPosts
Well, this is just hard-coded and it will not always be like this in real application because your values will often change and the most suitable place to handle these changes is inside a component. So in this example, we have just assumed that the updated posts were fetched from a database and have been updated, a more realistic example would be.
const posts = []
const AppContext = React.createContext({ posts });
const AppProvider = ({ children }) => {
const [updatedPosts, setUpdatedPosts] = React.useState(posts);
const getPosts = async () => {
const res = await fetch('some_api.com/posts');
const jsonRes = await res.json()
setUpdatedPosts(jsonRes.posts);
}
useEffect(() => {
getPosts() // not a good practice, only trying to make it short. see why below
}, [])
return (
<AppContext.Provider value={{posts: updatedPosts}}>
{children}
</AppContext.Provider>
)
}
const App = () => {
return (
<AppProvider>
<Header />
</AppProvider>
)
}
const Header = () => {
const { posts } = useContext(AppContext)
return (
<p>{posts.length}</p> {/* Whatever is returned from the api */}
)
}
Note: The "not a good practice" warning above is very important, you would naturally use the await keyword as it is an async function, and doing so in a useEffect
function would require proper subscribing and unsibscribing.
The returned provider component from our context app is just like any other component in React, and that is the reason we can use React state in AppProvider
in the example above. Let's see how to use states more effectively in React context
Using Reducers
React provides a useReducer hook that helps you keep track of multiple states, it is similar to the useState hook and it allows for custom state logic.
Because we will need to perform a series of updates in our context using states (like we did in the example above), it is often convenient to use the useReducer hook to handle all states in it.
The useReducer
function takes in two required arguments, the first being the reducer function and the second being the initial states for the hook. So every state you require in your context will (should) be included in the initial states. The function then returns an array containing the current state, and a function to handle the states (just like useState).
const reducer = (state, action) => {
if (action.type === 'TOGGLE_THEME') {
return { ...state, isDarkTheme: !state.isDarkTheme };
}
return state;
};
const App = () => {
const [state, dispatch] = useReducer(reducer, {
isDarkTheme: false,
});
const toggleTheme = () => {
dispatch({
type: 'TOGGLE_THEME',
});
};
return (
<div>
<h2>Current theme: {state.isDarkTheme ? 'dark' : 'light'}</h2>
<button onClick={toggleTheme}>Toggle theme</button>
</div>
);
};
In the reducer
function, the action parameter contains the object we passed to the dispatch function. The state parameter contains the current state of the initial state(s) passed into the useReducer
function. Don't worry if it's not so clear at first, you can always go to the docs and learn more for yourself.
The reducer
function should always return a state to act as the current states of the useReducer hook, it could simply be the unchanged state or updated state, but something has to be returned.
If we use this in our AppContext
to handle fetching posts we would have
const reducer = (state, action) => {
if (action.type === 'GET_POSTS') {
return { ...state, posts: action.payload };
}
return state;
};
const initialStates = { posts: [] }
const AppContext = React.createContext(initialStates);
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialStates)
const getPosts = async () => {
const res = await fetch('some_api.com/posts');
const jsonRes = await res.json()
dispatch({
type: 'GET_POSTS',
payload: jsonRes.posts
});
}
useEffect(() => {
getPosts() // not a good practice, only trying to make it short. see why below
}, [])
return (
<AppContext.Provider value={{posts: state.posts}}>
{children}
</AppContext.Provider>
)
}
const App = () => {
return (
<AppProvider>
<Header />
</AppProvider>
)
}
const Header = () => {
const { posts } = useContext(AppContext)
return (
<p>{posts.length}</p> {/* Whatever is returned from the api */}
)
}
Using context may not be clear yet, but it gets clearer as you develop with and in it. Now with that said let's see React context in action in our blog.
So paste the code below into AppState.js
import { createContext, useReducer } from 'react';
const appReducer = (state, action) => {
switch (action.type) {
case 'DELETE_POST': {
return {
...state,
posts: state.posts.filter((post) => post.id !== action.payload),
};
}
case 'ADD_POST': {
return {
...state,
posts: [action.payload, ...state.posts],
};
}
case 'SET_DARK_THEME': {
return {
...state,
darkTheme: action.payload,
};
}
default: {
return state;
}
}
};
const initialState = {
posts: [
{
id: 1,
title: 'Post One',
body: 'This is post one, do to it as you please',
},
{
id: 2,
title: 'Post Two',
body: 'This is post two, do to it as you please',
},
{
id: 3,
title: 'Post Three',
body: 'This is post three, do to it as you please',
},
{
id: 4,
title: 'Post Four',
body: 'This is post four, do to it as you please',
},
],
darkTheme: false,
};
export const AppContext = createContext(initialState);
export const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(appReducer, initialState);
const deletePost = (id) => {
dispatch({
type: 'DELETE_POST',
payload: id,
});
};
const addPost = (post) => {
dispatch({
type: 'ADD_POST',
payload: post,
});
};
const setDarkTheme = (bool) => {
dispatch({
type: 'SET_DARK_THEME',
payload: bool,
});
};
return (
<AppContext.Provider
value={{
posts: state.posts,
darkTheme: state.darkTheme,
deletePost,
addPost,
setDarkTheme,
}}
>
{children}
</AppContext.Provider>
);
};
The reducer function can always be moved to a different file, I for one always move it to a separate file it makes it cleaner for me.
The functions we create in this component will be useful outside the component so we can pass it as a value in the AppContext.Provider
component and any child of the AppProvider
component can access and use it. But this component does not have its effect yet till we wrap it around the App.js component, so let's make the updates
import './App.css';
import './App.dark.css';
import Header from './components/Header';
import PostList from './components/PostList';
import { AppContext, AppProvider } from './contexts/AppState';
function App() {
return (
<AppProvider>
<Header />
<AppContext.Consumer>
{({ posts, darkTheme, setDarkTheme }) => (
<>
<main className={`${darkTheme ? 'dark' : ''}`}>
<h3>
New Posts: <span>{posts.length} posts</span>
</h3>
<PostList />
</main>
<footer
onClick={() => setDarkTheme(!darkTheme)}
className={`${darkTheme ? 'dark' : ''}`}
>
<i className={`fas fa-${darkTheme ? 'sun' : 'moon'}`}></i>
</footer>
</>
)}
</AppContext.Consumer>
</AppProvider>
);
}
export default App;
While we can do this (i.e using the consumer component), we can also create components for both main
and footer
. But this is to illustrate to you that there will always be this option - the option of using the consumer component. There will be times when it would be the only option.
Finally, let's update the Header
component, all we need is the darkTheme
state and add it as a class name i.e
import { useContext, useState } from 'react';
import { AppContext } from '../contexts/AppState';
import AddPost from './AddPost';
const Header = () => {
const { darkTheme } = useContext(AppContext);
const [openModal, setOpenModal] = useState(false);
const closeModal = () => {
setOpenModal(false);
};
return (
<header className={`${darkTheme ? 'dark' : ''}`}>
<h1>DevBlog</h1>
<button onClick={() => setOpenModal(!openModal)}>Create Post</button>
{openModal && <AddPost closeModal={closeModal} />}
</header>
);
};
export default Header;
Reading Posts
The posts we have created in AppContext
has no effect yet because we are making use of the hard-coded posts array in components/PostList.jsx. So let's head there and make some changes.
Here, we only need to get the new posts array from AppContext
using the useContext hook. So replace
const posts = [{ id: 1, title: 'a title', body: 'a body' }];
with
const { posts } = useContext(AppContext);
You should ensure useContext
, and AppContext
are imported. Now save and test it out.
Adding Posts
Head up to components/AddPost.jsx and instead of logging into the console, pass in the post object into the addPost
function from our app context
import { useContext, useState } from 'react';
import { AppContext } from '../contexts/AppState';
const AddPost = ({ closeModal }) => {
const { addPost } = useContext(AppContext);
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const [error, setError] = useState(false);
const validateInputs = (e) => {
e.preventDefault();
if (!title || !body) return setError('All fields are required');
addPost({ title, body });
closeModal();
};
// .....
};
export default AddPost;
Save your app, and try adding a new post, you'd dis.
Deleting Posts
Just like we've done above, we will simply access the deletePost
function and pass it to the delete button. Recall the delete button is in components/PostItem.jsx, so add an onClick button like
//..
<i className='fas fa-trash' onClick={() => deletePost(id)}></i>
//..
Recall the deletePost function can be accessed as
const { deletePost } = useContext(AppContext);
With this, I believe you will be able to make the edit button functional, but if you want to go a little further you can add authors as part of the features. And for that, I'd recommend you create another context (UserContext
) to handle the authors' section. This context could contain functions like creating an author, logging an author in, updating the author's profile, listing all authors, and many more.
Conclusion
React context has its limits, for a simple blog like this it is the most suitable, but you may struggle to handle a web app that handles tons of changes with React context. But the community has great alternatives if you want to build the next Twitter, Netlify, or something. Some of which are Redux, Recoil, Remix, etc they will be discussed as part of a state management series that I have just started with this article. Remix is not a state management library, but it is a great alternative to it.
So far, we've created a blog that reads, deletes, and adds new posts with React context. This is a big step into React state management so go ahead and create cool apps with it.
You can tag me on Twitter @elijahtrillionz with a link to the blog you created as a result of this tutorial.
Kindly leave a comment below to let me know what you think of the series, please like and share for others to gain, and if you like what I do, you can show your support by buying me a coffee.
Posted on May 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 14, 2024