DEV Community

Akuma Isaac Akuma
Akuma Isaac Akuma

Posted on

The state of React's state management for me

Initially, Reacts global and components level state managements was once the most time-consuming part whenever I start a new project, It's always the "which one to use?" question.

Should I use redux or redux-saga or mobx-react or mobx-state-tree or several other packages, but haven't gone back and forth Reacts own Context API combined with Reducer has been the real winner for me.
No external package or no need to state learning any package APIs.

So let me share what my store looks like, both in ReactNative and Web.

// ~/lib/store.tsx
import { createContext, Dispatch } from "react";

export interface IState {
  user: User;
  notifications: Array<INotification>;
}

export type Actions = {
  kind: keyof IState;
  payload?: any;
};

export function appReducer(state: IState, action: Actions) {
  state = { ...state, [action.kind]: action.payload };
  return state;
}

interface IContextProps {
  state: IState;
  dispatch: Dispatch<Actions>;
}

export const AppContext = createContext({} as IContextProps);

export const AppStateProvider = ({ children }) => {

  const [state, dispatch] = useReducer(appReducer, {
    user: null,
    notifications: [],
  });

  return <AppContext.Provider value={{ state, dispatch }}>{children}</AppContext.Provider>;
};
Enter fullscreen mode Exit fullscreen mode

Then register the AppStateProvider at your app root

// ~/pages/_app.tsx
import { StateProvider } from "~/lib/store";

export default function App({ Component, pageProps }) {
  return (
    <AppStateProvider>
      <Component {...pageProps} />
    </AppStateProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Then usage will look like this

// ~/pages/index.tsx
import React, { useContext, useState } from "react";

export default function HomePage() {
  const { state, dispatch } = useContext(AppContext);
  const [form, setForm] = useState({ name: "", email: "" });

  function onChange(ev: React.FormEvent<{}>) {
    const target = ev.target as HTMLInputElement;
    const value = target.type === "checkbox" ? target.checked : target.value;
    const name = target.name;
    setForm((v) => ({ ...v, [name]: value }));
  }
  function login(ev: React.FormEvent<HTMLFormElement>) {
    ev.preventDefault();

    if (Object.values(form).every(Boolean)) {
      dispatch({ kind: "user", payload: form });
    } else {
      alert("Please fill the form well");
    }
  }
  return (
    <div>
      {state.user ? (
        <div>
          <hi>Welcome {state.user?.name}</hi>
        </div>
      ) : (
        <div>
          <h1>Login</h1>
          <form action="" method="post" onSubmit={login}>
            <section>
              <label htmlFor="name">Name</label>
              <div>
                <input name="name" id="name" value={form.name} onChange={onChange} />
              </div>
            </section>
            <section>
              <label htmlFor="email">Email</label>
              <div>
                <input name="email" id="email" value={form.email} onChange={onChange} />
              </div>
            </section>
            <button>Login</button>
          </form>
        </div>
      )}
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

And also know that the dispatch state key kind is well-typed

dispatch screenshot

The end!.
I hope this helps.

Top comments (7)

Collapse
 
marcelltoth profile image
Marcell Toth

While a great exercise, this is horrible for performance unfortunately and will not scale.
One of the hardest parts about React state management is managing re-renders.

A component in well written redux (or mobx or whatever, for that matter) app will only re-render if the parts of state it uses have changed. In redux this is achieved by writing good selectors, in mobx it is the automatic proxy that does that, which is the main selling point of the library.

In your example every component that uses AppContext will re-render each time every single thing changes. This is not scalable, and that's where libraries still come into play.

Collapse
 
akumzy profile image
Akuma Isaac Akuma • Edited

While what you said might be totally correct, you should not put a component level state in your global state
Whatever state field I have in my global store I am expecting it to affect my entire app so I don't think that's much of a big deal

Collapse
 
ivan_jrmc profile image
Ivan Jeremic • Edited

That is true if you use it wrong, but if you use context correctly it works fine at large scale and I would anyway never put server state in Redux that's why I use for server data ReactQuery and for client state context + useReducer, Redux is horrible when used for server data.

Collapse
 
ivan_jrmc profile image
Ivan Jeremic

I like to export a hook directly for usage of the store in your case add in store.tsx 'export const useAppState = () => useContext(AppContext);

You can now use it everywhere and it saves you imports in your components.

Collapse
 
akumzy profile image
Akuma Isaac Akuma

That makes sense thanks for the tips

Collapse
 
akumzy profile image
Akuma Isaac Akuma

Exactly which why I wrote the post to help others because I noticed a lot of people are still stuck with redux and others

Collapse
 
jeremiergz profile image
Jeremie Rodriguez

I understand your position but frankly, I'm on a project that implemented a solution similar to yours, and now that it's grown and become a bigger thing, the in-house solution starts to show its flaws...
As another commented, re-renders became problematic so a memoization layer had to be implemented, and now, as we want to use other stuff like device persisting, we must reinvent the wheel... that's why we plan on refactoring the whole thing to use a library...
Honestly, have a look at Redux Toolkit and you'll see that the most annoying stuff about Redux becomes way easier! 😊