DEV Community 👩‍💻👨‍💻

Cover image for React doesn't need state management tool, I said
Štěpán Granát for Tolgee

Posted on

React doesn't need state management tool, I said

From time to time someone still tells me that is using REDUX or similar tool in their project. I usually respond, that I wouldn't use it as now with hooks and context API you don't need it.

But context API usually brings performance problems and is also a bit awkward to use it properly, so today I'll try to show how to avoid common problems, and also build your own (micro) state management tool, without any compromises.

Naive solution

Basic idea is to manage state in one component and pass the whole it by context so it's accessible from all child components, so we can avoid props drilling.

export const StateContext = createContext(null);
const Provider = () => {
  return (
    <StateContext.Provider value={state}>
      <ChildComponent />
    </StateContext.Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Using dispatch

However you also need some way how to modify the state from children, you could pass individual functions to the context, but I personally don't like that as the state will get complex very fast. I like idea of dispatching events (similarly as in REDUX), so we basically pass one function which you can use to dispatch all different actions that you need. We could pass it through the same context as the state, but I don't like mixing it with the state, so I pass it through a separate context.

const StateContext = createContext(null);
const DispatchContext = createContext(null);

export const Provider = () => {
  const [state, setState] = useState(...)

  const dispatch = (action) => {
    switch (action.type) {
      case 'CHANGE_STATE':
        setState(action.payload)
        break;
      ...
    }
  }

  return (
    <StateContext.Provider value={{state, ...}}>
      <DispatchContext.Provider value={dispatch}>
        <ChildComponent />
      </DispatchContext.Provider>
    </StateContext.Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

I also like creating hook for getting the dispatch function to make it more explicit:

export const useDispatch = () => {
  return useContext(DispatchContext)
}
Enter fullscreen mode Exit fullscreen mode

Basically we are separating data from actions - provider component provides data to children. Children can dispatch actions to modify the data, but it's controlled by provider component, so it has control over it. Dispatched actions can be understood similarly as e.g. dom events, except we know who will receive it.

Image description

Now let's look at the performance side as if we want to use this as a replacement of REDUX, it needs to be able to handle big states with a lot of components subscribed.

Avoiding unnecessary children re-creation

In this configuration we are really inefficient, as all the children will get re-rendered every time we change something in the state. This happens because every time we update state in Provider component, all it's children will get re-created. We could use React.memo on children to avoid this, however nicer solution is to pass children from component above, so when the Provider is updated, children will stay the same. And we only update actual context consumers.

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

  ...

  return (
    <StateContext.Provider value={{state, ...}}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

In parent we do:

export const Parent = ({ children }) => {
  return (
    <Provider>
      <ChildComponent />
    </Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now the provider component is managing the context, but is not managing children (only passing them). It took me a while to understand this subtle difference as it is quite small change in the code, with big consequences.

The trick is to understand, that when we put <ChildComponent >, we are basically creating new React.Node every time, so all the children are re-rendered, unless we wrap them in React.memo.

So with this change, we update only components which are using the context.

Avoiding dispatch causing re-renders

Currently dispatch function is re-created every time the state is changed, which mean that all components using it will get re-rended, even though they are not using StateContext. Usually if we want to have stable function react documentation advices to use useCallback, but in this case it will help us only partially, because, that will basically cause "caching" of dispatch function and we wouldn't be able to use outer scope variables without including them into dependencies - and then the dispatch function would still get recreated when dependencies change. We will need to use ref to help us with this.

...

export const Provider = ({ children }) => {
  const [state, setState] = useState(...)

  const dispatchRef = useRef()

  // new function with every render
  const dispatchRef.current = (action) => {
    switch (action.type) {
      case 'CHANGE_STATE':
        // we can use outer scope without restrictions
        setState({...action.payload, ...state})
        break;
      ...
    }
  }

  // stable dispatch function
  const dispatch = useCallback(
    (action: ActionType) => dispatchRef.current(action),
    [dispatchRef]
  );

  return (
    <StateContext.Provider value={{state, ...}}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

This way stable dispatch function is passed to the DispatchContext and we can use outer scope without limitations.

Subscribable context

Last optimization we'll need is ability of the component subscribe only to part of the state. Now components can only use whole state and even when they need just small piece (e.g. one boolean value), they'll get notified every we change the state. This is not the best practice as we would still get unnecessary re-renders. The way to solve this is through use-context-selector.

This library is quite simple and it allows to use selector function, to "pick" what we want from the state.

import { createContext } from 'use-context-selector';

const StateContext = createContext(null);

export const Provider = ({ children }) => {
  return (
    <StateContext.Provider value={{state, ...}}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode
import { useContextSelector } from 'use-context-selector';

export const Subscriber = () => {
  const somePart = useContextSelector(StateContext, context => context.somePart)
}
Enter fullscreen mode Exit fullscreen mode

Oh, wait that is cheating! You said you will only use Context API!

This library is quite simple wrapper of React.Context api. It uses ref to wrap passed value, so that components are not re-rendering automatically and then it keeps list of subscribers. When value changes it runs all the subscribed functions and if the value from the selector is different than before it forces the subscribed Component to re-render. Similar concept is used e.g. in redux useSelector hook. So I say, it's quite standard solution and why build a new one, when it already exists?

PS: there is even a open RFC to add something like this directly into react

Final product

We can wrap this whole functionality to be reusable (+ add typescript types)

import React, { useCallback, useRef } from 'react';
import { createContext, useContextSelector } from 'use-context-selector';

type DispatchType<ActionType, DispatchReturn> = (
  action: ActionType
) => DispatchReturn;

type SelectorType<StateType> = (state: StateType) => any;

export const createProvider = <
  StateType,
  ActionType,
  DispatchReturn,
  ProviderProps
>(
  body: (
    props: ProviderProps
  ) => [state: StateType, dispatch: DispatchType<ActionType, DispatchReturn>]
) => {
  const StateContext = createContext<StateType>(null as any);
  const DispatchContext = React.createContext<
    DispatchType<ActionType, DispatchReturn>
  >(null as any);

  const Provider: React.FC<ProviderProps> = ({ children, ...props }) => {
    const [state, _dispatch] = body(props as any);
    const dispatchRef = useRef(_dispatch);

    dispatchRef.current = _dispatch;

    // stable dispatch function
    const dispatch = useCallback(
      (action: ActionType) => dispatchRef.current?.(action),
      [dispatchRef]
    );

    return (
      <StateContext.Provider value={state}>
        <DispatchContext.Provider value={dispatch}>
          {children}
        </DispatchContext.Provider>
      </StateContext.Provider>
    );
  };

  const useDispatch = () => React.useContext(DispatchContext);
  const useStateContext = (selector: SelectorType<StateType>) =>
    useContextSelector(StateContext, selector);

  return [Provider, useDispatch, useStateContext] as const;
};
Enter fullscreen mode Exit fullscreen mode

Usage example

type ActionType =
  | { type: 'CHANGE_STATE'; payload: ... }
  ...

export const [
  TranslationsContextProvider,
  useTranslationsDispatch,
  useTranslationsSelector,
] = createProvider(
  (props /* provider props */) => {
    const [state1, setState1] = useState(...)
    const [state2, setState2] = useState(...)
    const {data, isLoading} = useQuery(...)

    const dispatch = (action: ActionType) => {
      switch (action.type) {
        case 'CHANGE_STATE':
          setState(action.payload)
          break;
        ...
      }
    }

    const state = {
      state1,
      state2,
      data,
      isLoading
    }

    // don't forget to return state and dispatch function
    return [state, dispatch]
  })
Enter fullscreen mode Exit fullscreen mode

Lets summarize advantages of this solution:

  • Simple usage, nothing new to learn no boilerplate as with REDUX etc.
  • More efficient than Context api used naively
  • It scales as you have the whole power of hooks
  • You can use many instances and scope them only to the part of app that need them

In Tolgee.io, we use this on our most complicated view, where we handle translations table and we didn't have any problems with it yet.

What do you think?

PS: Check Tolgee.io and give us github stars

Top comments (68)

Collapse
 
dinsmoredesign profile image
Derek D

My whole reasoning for not using Redux is because Redux is overly complex for what it needs to be. While your solution works well for people that like Redux, you've essentially given no benefit to the developer for picking Context over something like Redux Toolkit, because it does a lot of these things behind the scenes for you already. Instead of setting up a specific way to use Context, you could just load in Redux Toolkit and be done with it - you mentioned this is less boilerplate but I'm not sure I see that?

Collapse
 
stepan662 profile image
Štěpán Granát Author

Maybe it's a bit confusing, that I'm using dispatch function. But I only named it this way as a convention, this differs completely from REDUX, I don't use anything like reducers, action creators, store etc. - that's what I mean by reducing boiler plate. I took dispatch as a way to send message to the Provider to make some mutation to the state, thats all.

Collapse
 
sgarciadev profile image
Sergei Garcia • Edited on

I don't use anything like reducers, action creators, store

But you literally just used implemented the Redux reducer pattern (useReducer hook, my mistake) which accepts a reducer function, and returns a dispatcher that accepts actions that will be created by action creators as the project scales. You didn't reduce boilerplate, you just re-created the entire Redux logic without the store setup.

I took dispatch as a way to send message to the Provider to make some mutation to the state, thats all.

Ummm... you just described how Redux works 😅 except you are now forced to write action creators manually to avoid typo issues, when Redux Toolkit could be generating all the action creators for the reducer for you, see:

import { createSlice, configureStore } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment(state) {
      state.value++
    },
    decrement(state) {
      state.value--
    },
    incrementByAmount(state, action) {
      state.value += action.payload
    },
  },
})

const store = configureStore({
  reducer: { counter: counterReducer },
})

store.dispatch(counter.reducer.actions.increment())
store.dispatch(counter.reducer.actions.decrement())
store.dispatch(counter.reducer.actions.incrementByAmount(2))
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
stepan662 profile image
Štěpán Granát Author

I don't mean to be disrespectful, but I think you don't understand the article. I haven't used useReducer anywhere and I didn't even duplicate it's logic (if that's what you mean). Also I didn't recreated whole redux logic (that would be quite a challenge :D). I don't think that writing action creators is necessary - if you use typescript it should prevent typos, I've suggested that in the article. All in all, I'm glad that you are interested into this topic, but it feels like you are arguing against things that I haven't wrote in the article.

Thread Thread
 
sgarciadev profile image
Sergei Garcia • Edited on

My apologies, you are absolutely right! I missunderstood you reducer implementation using useRef with useReducer given it looked so much like one. However, you did recreate the reducer and action creator part of it, which were still my original points. See your usage example where you literally wrote something extremely similar to a Redux Reducer and action dispatching:

type ActionType =
  | { type: 'CHANGE_STATE'; payload: ... }
  ...

export const [
  TranslationsContextProvider,
  useTranslationsDispatch,
  useTranslationsSelector,
] = createProvider(
  (props /* provider props */) => {
    const [state1, setState1] = useState(...)
    const [state2, setState2] = useState(...)
    const {data, isLoading} = useQuery(...)

    const dispatch = (action: ActionType) => {
      switch (action.type) {
        case 'CHANGE_STATE':
          setState(action.payload)
          break;
        ...
      }
    }

    const state = {
      state1,
      state2,
      data,
      isLoading
    }

    // don't forget to return state and dispatch function
    return [state, dispatch]
  })
Enter fullscreen mode Exit fullscreen mode

I don't think that writing action creators is necessary - if you use typescript it should prevent typos

And there's your problem. You are making the assumption everyone uses typescript now. And it's yet another reason why people build action creators... because they just work at ensuring consistent actions types, independent of if you're using typescript or not.

it feels like you are arguing against things that I haven't wrote in the article.

You opened your article with "React doesn't need state management tool {...} From time to time someone still tells me that is using REDUX or similar tool in their project. I usually respond, that I wouldn't use it as now with hooks and context API you don't need it.". That's a pretty big and powerful statement you opened your article with which implies you have an objectively better state management solution than redux in all scenarios. And not just that, but you only listed advantages to your solution, with zero disadvantages.

Just as a heads up, but if you want to make bold claims like that, you need to be prepared to back them up 🙂 And not be surprised when people start pointing out the flaws in your proposed alternative. If you had worded your article's title to "Building your own redux-like state management solution", you can be sure you wouldn't have gotten half as constructive feedback as this 😉 Since at no point would you claim it's a better solution than Redux, just a different one that might be useful for some people. Still, I don't blame you. Hyperbole-like, clickbait-y titles are usually what drives clicks 🤷‍♂️ Can't hate the player, hate the game.

Thread Thread
 
stepan662 profile image
Štěpán Granát Author

Yep, I opened the article with a bit of provocation, that's true. I also made sure, it's clear that it's only my personal opinion. But I truly think that this way of managing the state can remove the need of REDUX. I don't say, that it's just this little piece of code that I wrote, but basically instead of using REDUX for everything (state, API calls, forms ...), we use hook libraries like react-query, formik etc. I'm sure people are doing this all the time. However sometimes there is a need for a bit of complex state management and that is what we use this solution for - it enables you to combine multiple hook libraries and create a piece of state, that is:

  • shared with children (avoiding props drilling)
  • local (lives and dies with the provider)
  • and performant (you update only components that need to)

Also, I'm not really using reducer as is defined by REDUX/FLUX. I think real reducer should get the action and return new state, that's not what I'm doing. I'm only recieving actions, but modifying the state through callbacks like setState. It's a subtle difference I know, but this allows you to use it with react-query and not to keep the whole state on one place as "proper" reducer would.

Thread Thread
 
sgarciadev profile image
Sergei Garcia • Edited on

Totally fair and agreed. Sorry if my original comment was a little direct.

Btw, but just to end with 0 chance of doubt, did you try Redux Toolkit before making this or writing the article? 🤔 Curious because of this statement:

I'm not really using reducer as is defined by REDUX/FLUX. I think real reducer should get the action and return new state, that's not what I'm doing. I'm only recieving actions, but modifying the state through callbacks like setState.

Reducers made with Redux Toolkit don't return new state either, they only receive an action and mutate state (thanks to an immer compat layer), exactly like you described 😅 See: redux-toolkit.js.org/api/createSlice

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  // "real" react redux toolkit reducer
  reducers: {
    increment(state) {
      state.value++ // state mutation 🙂
    },
    decrement(state) {
      state.value-- // another state mutation!
    },
    incrementByAmount(state, action) {
      state.value += action.payload // notice how we don't return anything here 👀
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

If you weren't aware of that, I wonder if this could explain the confusion from all sides 😅 And if so, I hope this explains why we all feel like you just wrote a 1:1 "lite" version of Redux + Redux Toolkit.

Thread Thread
 
stepan662 profile image
Štěpán Granát Author

Don't get me wrong I think there are still use cases for REDUX and if I ever find myself in a position that I will need that, I'll definitely try redux toolkit or some similar tool.

However I still don't agree that my solution is the same as redux toolkit. I see redux toolkit as a "syntax sugar" for redux to reduce boiler plate, that's fine. However, my main complain is that REDUX brings unnecessary complexity for majority of projects, you can cover up the complexity with tools like redux toolkit. But from my experience it's better to avoid the complexity from the beginning.

I don't think I've re-built redux toolkit, but if so, that would be actually quite bad sign for redux - think about it ... You take quite a complex library, which you need additional toolkit to be usable and you end up having same solution as writing 30 lines of code.

.
.
.

Last part is exaggerated of course :)

Thread Thread
 
murkrage profile image
Mike Ekkel

You don't need the toolkit for it to be usable. The toolkit is an opinionated way of setting up and using redux but it's just as easy to use redux without it. The power of the toolkit is that you'll be familiar with all other implementation of the toolkit whereas Redux itself gives you a lot of freedom to set it up however you like.

A good example is the addition of Immer in the toolkit. It's absolutely not necessary for Redux but you could implement it yourself without the toolkit if you wanted to.

Collapse
 
sgarciadev profile image
Sergei Garcia • Edited on

This article would be fantastic.... if it had released 2 years ago before Redux Toolkit came out, which sorted out the boilerplate issue for the most part.

I'd hesitate to call this a more scalable solution than something like Recoil or Redux Toolkit since you still need to have a deep understanding of how context affects rendering and performance. And it's why context will always be better suited for sharing primarily static data that won't change, as opposed to a state management tool. Not to mention that it doesn't account for any of the more advanced state management features like middleware and async operations.

This seems great for prototyping small to medium size projects, but not much else. Still, it's definitely neat and worthwhile mentioning. Too many people feel like they need to use a massive state management library like Recoil or Redux for their hobby project when something leaner could be enough.

Collapse
 
stepan662 profile image
Štěpán Granát Author

I wouldn't say that REDUX scales perfectly with big projects. Having one huge state is not always ideal, especially in cases when you have multi page app, then you have parts of your state, which are completely useless for some pages, but they can still ask data from there, because the state is global. I feel, that having limited context provided only to e.g. one page, scales much better as other pages can't access it even if they want to.

Collapse
 
sgarciadev profile image
Sergei Garcia

Having one huge state is not always ideal, especially in cases when you have multi page app, then you have parts of your state, which are completely useless for some pages, but they can still ask data from there, because the state is global.

Why would state "ask for data" if the pages are not being used? This sounds less like a Redux problem, and more like an architectural problem caused by bad developer planning. Redux state not-in-use doesn't affect memory or performance in any measurable way. And if it does, it's because you probably are doing some kind needless logic somewhere which could also be caused using the limited context pattern.

I feel, that having limited context provided only to e.g. one page, scales much better as other pages can't access it even if they want to.

This isn't a benefit exclusive to limited context. You can achieve the same results by having individual reducers for each page and not subscribing to global changes from other pages. If you want to have state that can't be accessed by other pages... maybe just don't access it from other pages? You make it sound like because it's global, you are forced to use it everywhere and that's just not true.

What confuses me the most, is your article basically ends with you building a lighter, but more limited version of Redux by combining Context + useReducer + use-context-selector. You basically re-invented the modern wheel, except it's lighter, but harder to repair and can only drive on certain roads (and not very fast). Which is great and impressive as a learning exercise, don't get me wrong. But it's missing important key functionality from global state management systems such as middleware, async operation handling, among other things. And calling it a "redux replacement" is a bit of an overstatement.

Other than that, it's a good first article on Dev.to, congrats!

Thread Thread
 
stepan662 profile image
Štěpán Granát Author

Sure, it really depends on the project though. I understand that in long lasting projects it is really important to have some strong foundation, where REDUX can give you that, it's a tested approach and you know that it won't kick you.

In our case we wanted to be efficient and reduce unnecessary boilerplate, while keeping the performance and the fact that you can build it yourself pretty easily was quite amazing for me :)

Thread Thread
 
sgarciadev profile image
Sergei Garcia

That's fair! I agree that Tolgee seems like a rather simple app where perhaps Redux would have been overkill. Kudos to you for taking the initiative and building your own state management solution, I agree it was probably a fun learning experience.

My feedback was merely directed at that this wasn't a viable solution for Redux for anything larger than small projects 😅 Thanks for understanding. Have a good day!

Thread Thread
 
jancizmar profile image
Jan Cizmar

I don't know what is the size apps you usually build, but I wouldn't say that Tolgee is simple app. Actually we decided to stop using Redux, because it grew too much. ⚰️

Collapse
 
ivanjeremic profile image
Ivan Jeremic

redux toolkit is not clean or can claim it uses less boilerplate than, recoil, valtio or similar state managers.

Collapse
 
ivanjeremic profile image
Ivan Jeremic • Edited on

I'm more in the hookstate, recoil camp. I just don't like reducers.

Collapse
 
stepan662 profile image
Štěpán Granát Author

I guess it's a lot about personal taste. My goal was to sum up, how to use context in efficient way - but keep it as simple as possible. I basically understand dispatch as a way how to send message to the provider to modify the state. And I like that this way you can pass one function, which can handle all the events, that's how I think about it.

Collapse
 
sgarciadev profile image
Sergei Garcia

But this would mean you would now need to create action creators to ensure it scales properly... which is more boilerplate. Boilerplate you wouldn't need to write with Redux Toolkit slices 🤷‍♂️ To each their own I guess.

Thread Thread
 
lexiebkm profile image
Alexander B.K.

What motivates me further to use Redux is that Redux Toolkit (RTK) provides new feature called RTK Query, so that we can define endpoints more directly and intuitively using createApi (redux-toolkit.js.org/rtk-query/api...), instead of previous ways using createSlice, createAsyncThunk, etc.

Thread Thread
 
ivanjeremic profile image
Ivan Jeremic • Edited on

I would not use RTK, try Valtio + react query

Collapse
 
ivanjeremic profile image
Ivan Jeremic • Edited on

Yes, but if you think more about it sending a message like

dispatch({type:"add_todo", payload})

you can get the same reducer like behavior with a normal dedicated function like

function addTodo(payload) {
Set...
Set...
do something else...
}

So the more you think about it you will see the complexity is bringing you no benefits.

Thread Thread
 
stepan662 profile image
Štěpán Granát Author

You can easily do this through context too. I would say, that it's just matter of taste. I'd say I chose dispatch, because I see the symbolic of "dispatching event" for the provider, which is the way I like thinking about it. But I don't really have a strong opinion on this ...

Collapse
 
aytacg26 profile image
Aytac Güley

Instead, I prefer Redux toolkit, in any case, we need a kind of complex boilerplate, but with Redux toolkit, I think, that headache was solved. Until Redux toolkit, in small to medium projects, Context API was great for me and it is a part of React, which makes Redux unnecessary but for large projects, I believe currently, Redux toolkit can be accepted as the best, I cannot say it is the best because there can be always a better way.

Collapse
 
thereis profile image
Lucas Reis

All of those articles pretend to copy the same names and patterns as redux, so why don’t you go to redux toolkit instead? Contexts solve problems on small projects, but since the complexity is constantly expanding, redux is way simpler to make it more simpler do debug and increment functionalities.

Collapse
 
stepan662 profile image
Štěpán Granát Author

The problem is that I really like using hook libraries like react-query and those are not designed to cooperate with REDUX. I think that's also a reason why many people just want to use context, because you keep whole power of hooks in there and you can just pass data to all children.

Thread Thread
 
buzziebee profile image
Tom Bee

You should check out rtk query. It's part of redux toolkit now and is fantastic. I use the rtk query codegen to automatically create the entire interface with my backend from the swagger. It builds all the redux integration as well as properly typed hooks to use in components.

Thread Thread
 
lexiebkm profile image
Alexander B.K.

Yeah... RTK Query seems to make our task in defining endpoints for data fetching easier or more directly/intuitively. Instead of using createSlice and createAsyncThunk for that purpose, we can just use createApi and fetchBaseQuery.

Collapse
 
alanxtreme profile image
Alan Daniel

Recoil ftw

Collapse
 
benstov profile image
benstov • Edited on

Check out recoil on bundle phobia, i wouldn't use it with how much it weights, unless your website shouldn't appear on Google.
Bundle size is important, and there is a reason why recoil is in experimental state.

If they could make it weigh 2kb I might consider it.

Collapse
 
bennodev19 profile image
BennoDev • Edited on

Recoil is heavy, but it also saves you from writing a lot of boilerplate code.
However, there are also lightweight (tree-shakable) alternatives
with a similar approach.

Collapse
 
stepan662 profile image
Štěpán Granát Author

I haven't used recoil yet. But from documentation it feels like it's a bit more complicated. I tried to keep it simple and similar to React.Context API. But I guess it has very similar use case ...

Collapse
 
zachbryant profile image
Zach

Imo using it feels way simpler than context. Different api than context, but very very little setup involved

Collapse
 
sa_webb profile image
Austin Webb

We implemented this pattern across many Next.js apps without issue but our team decided that we wanted to adopt a state tool. Our choices were between Recoil, Jotai, and Zustand. We ended up going with Zustand and everyone seems to love it. However, I’d really like to ramp up with Recoil.

Nice article!

Collapse
 
spock123 profile image
Lars Rye Jeppesen

Setting a bunch of hooks and effects in React components really shows what a big mess React is, compared to more elegant solutions with Angular and Vue.

Typed Observable Reactive stores is where it's at and the APIs for using those are much more composable and beautiful. I can't believe React has become such a mess, but I guess that's why it's the oldest of the big frameworks.

Collapse
 
krtirtho profile image
Kingkor Roy Tirtho

I'm crying in zustand. It's much simpler to setup & doesn't impact the bundle size much. Now everyone (REDUX FANBOIS) will say redux-toolkit makes setting up redux easier but that's what redux itself should do. Why would I add another library over already installed redux & react-redux summing up my total bundle size with another 40KB?

So I now agreed on a combo of ZUSTAND & React-Query which is highly recommended for others too (IMO)

Collapse
 
dkamyshov profile image
Danil Kamyshov

The problem with React Context is that (due to the nature of React itself) it behaves like a state management tool, while in reality it is a tool for dependency injection. This confusion often leads to the idea to use React Context as a state manager, which it was never intended for.

If you take a closer look at any decent state management solution, you will quickly notice that these tools never pass "naked" state (values that change frequently) directly via context, they instead wrap the state with "something" and then pass this "something" through the context. This "something" is generally called "an observable store".

Different tools use different names ("store" - redux, effector, zustand, etc.; "client (cache)" - apollo-client; "query client (cache)" - react-query), but the end result is the same - "naked" state is wrapped and the referential equality is preserved even when the state changes.

So, instead of using cryptic libraries like use-context-selector, it might be a better idea to take advantage of the "observable store" pattern. If you don't feel like using an existing solution, you can easily implement everything yourself, it's like ~30 LOC with all the infrastructure.

Collapse
 
dongnhan profile image
dongnhan

Nice article, I did not know that separating contexts and their childrens help like that

I think React offers useReducer which guarantees the dispatch function to be stable reactjs.org/docs/hooks-reference.h...

Collapse
 
stepan662 profile image
Štěpán Granát Author

Yes, but it's kinda different pattern. I like that here you can combine your custom state with hooks and their state ... we use react-query and this way, you can do things like this:

const users = useQuery(...)
...
case 'REFETCH_DATA':
  users.refetch()
...

state = {
  ...
  usersData: users.data
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
dongnhan profile image
dongnhan

Agree, that's a point and I think it really depends on the need and state architecture of an app. Reducer my in opinion is usually helpful when you have some actions mutating different interconnected states per dispatch.

I used react-query in my project too and I like to wrap useQuery/useMutation under a custom hook that specialize for a business domain.
For example:

const useFinance = () => {
  const  { id } = useUserInfo();
  const financeDataQuery = useQuery('financeData', financeService.getFinanceData);
  const updateFinanceDataMutation = useMutation((cashBalance) => financeService.updateFinanceData({id, cashBalance}));
  return {
    financeDataQuery,
    updateFinanceDataMutation,
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
johnyepthomi profile image
JohnYepthomi • Edited on

I also use context API and I'm amazed when people are not aware of what it can do and I'm relatively new to the react world. I haven't tried Redux yet and so far I haven't come around to needing it ....YET

Collapse
 
sgarciadev profile image
Sergei Garcia

People are aware of what it can do, but it's not used because it's well known React Context was not made for sharing frequently changing state. Because React Context triggers re-renders by default (unless you add custom logic to prevent it, like the author), it's frequently a common cause for performance issues.

It's why React Context's main use is to share static, non-changing data.

Collapse
 
johnyepthomi profile image
JohnYepthomi

That's what I said, the possibility of adding your custom logic using the context API is what I'm talking about. What else would I be referring to and my statement wasn't a blanket statement, the key word being "WHEN" because I have encountered people that don't even have an idea about this aspect. Anyways, I am aware of the benefits of using other libraries. But for smaller projects , this will suffice. There's no need to further bloat it up. It comes down to the requirements of the project.

Thread Thread
 
sgarciadev profile image
Sergei Garcia

Ah, I see what you mean. The way you worded it made it sound like you thought people didn't know it could be used for state management (given that's what the article you commented on is entirely about). I was simply replying that it's pretty well known it can be used, just that people prefer not to use or write articles about it since the performance considerations make it somewhat of a foot-gun (a gun prone to miss-fires that you shoot yourself in the foot with).

Also worth mentioning, but the performance considerations could also present themselves even in smaller projects, if you have enough expensive-to-render components depending on the context. Meaning even for smaller projects, Context still might not be the best option.

I agree with you that it comes down to the requirements of the project though.

Collapse
 
stepan662 profile image
Štěpán Granát Author • Edited on

I'd say, that not if you are not using any library is easier to make things messy or slow. It's good to be aware how exactly context work, think about where to use it and how split the state - however if you do that it's quite rewarding.

Collapse
 
pstev profile image
Petar Stevovski

This question is not related to Redux vs Context, but would be glad if someone takes the time to answer.

It seems to be a "me" issue, but when and how to decide if I even need a global state management tool at all? I'm currently working on a pretty big project, that will progress in scale as time goes by, but so far, I haven't really felt the need of a global state management..?

We're using react-query for API-related data, and I'm using Context API for simple use cases such as having the sidebar menu of the dashboard opening/closing when clicked from an outside component which triggers update to the whole dashboard, and also using the Context API for i18n language changes.

Besides that, I haven't really felt the need for anything else. Or am I looking incorrectly at the things, and missing something? Haven't really thought of a scenario so far where I'll need something like Redux at all. What are some examples of such scenarios?

Collapse
 
dikamilo profile image
dikamilo

Redux may be handy when you want collaborative UI (app like google docs), and want to update actions live for all users because of that event sourcing pattern that Redux use. You just need to pass that actions you're using over websocket and apply to the store.

Because Redux have strong convention it may be plus whey you switch between projects or your project have high devs rotation. Evebody are familiar with this solution so it's easier to work with it. React context can be used diferently depending what developers do.

Also, Redux is easy to debug with dedicated devtools. You can see what happened as the new condition is calculated and you can "rewind time". So during development, developement team with QA team, QA can actually send you serialzied store (or just post it for example in Jira ticket as part of issue/bug) and you can reproduce exact same state of the app. It can be really handy.

In React Contexts, we have standard react devtools and we can only view components.

React Contexts should be modular, but devs often are afraid of having too many Contexts, so they stuff extra responsibilities into existing Contexts, which make them grow very much.

And if we already have a lot of them, there may be chaos in the organization of data flow, and in particular what from whom it depends, because there is no one source of truth.

And finally chaos in tests, because different components can depend on different contexts and it is not visible from the outside.

All of that solutions are just tools and you need pick the best, that fits you need.

Collapse
 
ozzythegiant profile image
Oziel Perez

I understand Redux has quite a bit of boiler plate but honestly, I hate Hooks so much and wish it never came out. Too many devs are writing sloppy code with them and large projects are getting extremely hard to manage and debug simply because there is no standardization of how to structure custom hooks. Never using Hooks and will continue to use Redux. Only exception is a quick component that needs useState but that's about it.

Collapse
 
aralroca profile image
Aral Roca • Edited on

What do you think about Teaful 😊?

github.com/teafuljs/teaful

I think it's even less boileplate, and you still avoid more rerenders, since it only rerenders if the store property being consumed is updated, and not others.

Collapse
 
behemoth11 profile image
Behemoth11

I developed a project using Next Js. I got away without the need to use redux by implementing a globals context and other smaller context. Instead of a redux like approach I went for an object oriented one. I created hooks for every functionality of the app: e.g ( a hook for a user that has login, logout etc) . When I am want to use a peace of state I just call the useGlobalContext and get the object I want. I am then able to use every functionality from the object I have created. It helps to create different hooks because it makes scaling very easy.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.

👋 Every week new members join DEV and share a bit about them in our Welcome Thread

Welcome them to DEV and share a bit about yourself