DEV Community

Cover image for React: ContextAPI as a State solution?
Dewald Els
Dewald Els

Posted on • Edited on

React: ContextAPI as a State solution?

πŸ‘¨β€πŸ’» Github Repository

If you just want to take a peek at the code, here is a repository I used for the article.

Github repository


πŸ“ Premise of this article

The following content is purely experimental and by no means implies that it is "best-practice" or "this is how it should be done". I'm trying to become more familiar with React and these experiments help me see my own failings and misunderstandings of the framework.


πŸ’¬ Feedback

I love receiving feedback from this awesome community and learn so much from the advice or resources given.


Context API

The React ContextAPI was introduced, to my understanding, NOT to replace state management, but rather, easily share props down the component tree. This made the ContextAPI a great way to avoid the "prop-drilling" problem. If you'd like to know more about that, I can highly recommend the blog post on prop drilling by Kent C. Dodds.


πŸ§ͺ The Experiment

Given the design of the ContextAPI, I thought, perhaps it COULD be used for sharing and updating state. The ContextAPI has a Provider that can be wrapped around any component, to expose the data you'd like to pass down the component tree.

If you're interested in seeing what i came up with, please read on. πŸ‘


1. Setting up the AppContext

The first order of business, was to create a Component that I could wrap around my App. This Component should be the Context Provider where I can share my state and a function to update the state from anywhere in the app.

import {createContext, useState} from "react";

const AppContext = createContext();

const AppProvider = ({children}) => {

    const [state, setState] = useState({
        profile: null,
        uiLoading: false,
        movies: []
    });

    return (
        <AppContext.Provider value={{state, setState}}>
            {children}
        </AppContext.Provider>
    );
}
export default AppProvider;
Enter fullscreen mode Exit fullscreen mode
src/AppContext.js

This allowed me to easily wrap the AppProvider component around my entire app, as seen below.

...
import AppProvider from './AppContext';

ReactDOM.render(
    <React.StrictMode>
        <AppProvider>
            <App/>
        </AppProvider>
    </React.StrictMode>,
    document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode
src/index.js

2. Reading state using a Custom Hook

Although the above worked okay, trying to update or even read the state felt very cumbersome.

The component would need to get the entire state object out of the Provider and then use state.propName when reading from the state object.

Therefor, I created a custom hook called useAppState that accepted a reducer function to get a specific state property from the state object.

export const useAppState = (reducer) => {
    // Destructure State object from Context
    const { state } = useContext(AppContext);
    return reducer(state);
}
Enter fullscreen mode Exit fullscreen mode
src/AppContext.js

This allowed me to use the following code to read any property from my state object.

...

function App() {
    console.log('App.render()');

    // READ FROM APP STATE
    const profile = useAppState(state => state.profile);

    return (
        <main>
            <h1>Another Movie App</h1>
        </main>
    );
}
export default App;
Enter fullscreen mode Exit fullscreen mode
src/App.js

If I need to get multiple items from state I could simply destructure from the entire state object, or write multiple lines to get the property I need.

// Using destructring
const { profile, movies } = useAppState(state => state);

// Multiple lines
const profile = useAppState(state => state.profile);
const movies = useAppState(state => state.movies);
const uiLoading = useAppState(state => state.uiLoading);
Enter fullscreen mode Exit fullscreen mode
src/AnyComponent.js

I've noticed that using multiple-lines does create a duplicate AppContext object in the React developer tools. Every component that uses this function seems to get a duplicate Hook entry under hooks

Although, I'm not sure if this is only a visual indication or if the state objects are actually duplicated in the component. See below:

Alt Text

Apparent duplication of State objects (Not sure if it is the case)

3. Dispatch function to update state

The next step was to improve the developer experience when updating the state. Even though the set state worked fine, it wasn't a great experience having to destructure form the AppContext and constantly having provide the current state and the new state.


// Custom Hook to easily access dispatch function.
export const useDispatch = () => {
    const {dispatch} = useContext(AppContext);
    return dispatch;
}

const AppProvider = ({children}) => {

    const [state, setState] = useState({
        profile: null,
        uiLoading: false,
        movies: []
    });

    // Reusable function to update state
    const dispatch = (state) => {
        setState(currentState => {
            return {
                ...currentState,
                ...state
            };
        });
    }

    // Remove setState from value and replace with dispatch function.
    return (
        <AppContext.Provider value={{state, dispatch}}>
            {children}
        </AppContext.Provider>
    );
}
export default AppProvider;
Enter fullscreen mode Exit fullscreen mode
src/AppContext.js

After making the above changes, I could now easily get the dispatch function from the AppContext using the Custom Hook.

As an example, if I wanted to update the profile, I could do use something like this:

import {useDispatch} from "../../AppContext";

const Login = () => {

    // Get dispatch from AppContext
    const dispatch = useDispatch();

    const onLoginClick = () => {
        dispatch({
            profile: {
                name: 'Bird Person',
            }
        });
    }

    return (
        <main>
            <button onClick={ onLoginClick }>Login</button>
        </main>
    )
}
export default Login
Enter fullscreen mode Exit fullscreen mode
src/components/Login/Login.js

The above code shows that you can simply pass in an object with the properties relating to the state you'd like to update.

Any component that uses the useAppState hook would also be re-rendered with the updated state.

Edit: 21.05.2021 πŸ“‹ This behaviour could also be a caveat of the solution. As pointed out by @dikamilo in the comments, ANY component that uses the useAppState will reload. This severely limits app scalability.
End Edit

You can now also, quite easily, update multiple state values using this pattern:

...

dispatch({
    movies: response.data,
    uiLoading: false,
});

Enter fullscreen mode Exit fullscreen mode

This is the basis of the idea. You can of course do a lot more to optimise and improve the code.


πŸ”Ž Findings

I found that the lack of debugging tools makes this a poor choice for real-world applications. If you would like to make a small prototype, or a simple app that has very little state, the approach could work fine. Beyond that I can't say that this would be a great development experience due to the lack of debugging.

You can track the state in the React DevTools.

I would however not consider using this as a solution above established state management tools already available.


Have you ever tried something like this before? Do you have a better approach. Feel free to add comments or insights you might have!


πŸ€“ Thanks for reading πŸ™

Top comments (2)

Collapse
 
dikamilo profile image
dikamilo

As you're mentioned, any component that uses useAppState will be re-rendered when state changes, and this is because you're grouped all state object into single Context. This may be a big drawback in application, so you're may consider splitting state into multiple Contexts.

Sometimes separate state solution is too big gun for the problem, when you're having a little state to manage in application and Context API may resolve this problem.

And if you keep data from the backend in your global store, you may consider to use for example react-query that will cache your data without global store.

Collapse
 
dewaldels profile image
Dewald Els

Yeah, that is one major drawback for this approach! I’ve only used Redux as a state management solution. I’ll certainly look into react-query.