How To Build An App With React Context API

elijahtrillionz

Elijah Trillionz

Posted on May 30, 2022

How To Build An App With React Context API

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}>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 */}
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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 */}
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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' }];
Enter fullscreen mode Exit fullscreen mode

with

const { posts } = useContext(AppContext);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
//..
Enter fullscreen mode Exit fullscreen mode

Recall the deletePost function can be accessed as

const { deletePost } = useContext(AppContext);
Enter fullscreen mode Exit fullscreen mode

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.

Buy me a coffee

πŸ’– πŸ’ͺ πŸ™… 🚩
elijahtrillionz
Elijah Trillionz

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