B. Cole
B. Cole

Reputation: 134

React - Hooks + Context - Is this a good way to do global state management?

I am trying to find a good, clean, with little boilerplate, way to handle React's global state

The idea here is to have a HOC, taking advantage of React's new Hooks & Context APIs, that returns a Context provider with the value bound to its state. I use rxjs for triggering a state update on store change.

I also export a few more objects from my store (notably : the raw rxjs subject object and a Proxy of the store that always returns the latest value).

This works. When I change something in my global store, I get updates anywhere in the app (be it a React component, or outside React). However, to achieve this, the HOC component re-renders.

Is this a no-op ?

The piece of code / logic I think could be problematic is the HOC component:

const Provider = ({ children }) => {
    const [store, setStore] = useState(GlobalStore.value)

    useEffect(() => {
        GlobalStore.subscribe(setStore)
    }, [])

    return <Context.Provider value={store}>{children}</Context.Provider>
}

GlobalStore is a rxjs BehaviorSubject. Every time the subject is updated, the state of the Provider component gets updated which triggers a re-render.

Full demo is available there: https://codesandbox.io/s/qzkqrm698q

The real question is: isn't that a poor way of doing global state management ? I feel it might be because I basically re-render everything on state update...

EDIT: I think I have written a more performant version that's not as lightweight (depends on MobX), but I think it generates a lot less overhead (demo at: https://codesandbox.io/s/7oxko37rq) - Now what would be cool would be to have the same end result, but dropping MobX - The question makes no sense anymore

Upvotes: 1

Views: 1479

Answers (2)

Erick Vieira
Erick Vieira

Reputation: 11

I understand your need to handle a global state. I already found myself in the same situation. We have adopted similar solutions, but in my case, I've decided to completelly drop from ContextAPI.

The ContextAPI really sucks to me. It seems to pretend to be a controller based pattern, but you end up wrapping the code inside an non-sense HOC. Maybe I've missed he point here, but in my opinion the ContextAPI is just a complicated way to offer scoped based data flow.

So, I decided to implement my own global state manager, using React Hooks and RxJS. Mainly because I do not use to work on really huge projects (where Redux would fit perfectly).

My solution is very simple. So lets read some codes because they say more than words:

1. Store

I've created an class only to dar nome aos bois (it's a popular brazilian expression, google it 😊) and to have a easy way to use partial update on BehaviorSubject value:

import { BehaviorSubject } from "rxjs";

export default class Store<T extends Object> extends BehaviorSubject<T> {
  update(value: Partial<T>) {
    this.next({ ...this.value, ...value });
  }
}

2. createSharedStore

An function to instantiate the Store class (yes it is just because I don't like to type new ¯\(ツ)/¯):

import Store from "./store";

export default function <T>(initialValue: T) {
  return new Store<T>(initialValue);
}

3. useSharedStore

I created an hook to easily use an local state connected with the Store:

import Store from "./store";
import { useCallback, useEffect, useState } from "react";
import { skip } from "rxjs/operators";
import createSharedStore from "./createSharedStore";

const globalStore = createSharedStore<any>({});

type SetPartialSharedStateAction<S> = (state: S) => S;
type SetSharedStateAction<S> = (
  state: S | SetPartialSharedStateAction<S>
) => void;

export default function <T>(
  store: Store<T> = globalStore
): [T, SetSharedStateAction<T>] {
  const [state, setState] = useState(store.value);

  useEffect(() => {
    const subscription = store
      .pipe(skip(1))
      .subscribe((data) => setState(data));
    return () => subscription.unsubscribe();
  });

  const setStateProxy = useCallback(
    (state: T | SetPartialSharedStateAction<T>) => {
      if (typeof state === "function") {
        const partialUpdate: any = state;
        store.next(partialUpdate(store.value));
      } else {
        store.next(state);
      }
    },
    [store]
  );

  return [state, setStateProxy];
}

4. ExampleStore

Then I export individual stores for each feature that needs shared state:

import { createSharedStore } from "hooks/SharedState";

export default createSharedStore<Models.Example | undefined>(undefined);

5. ExampleComponent

Finally, this is how to use in the component (just like a regular React state):

import React from "react";
import { useSharedState } from "hooks/SharedState";
import ExampleStore from "stores/ExampleStore";

export default function () {
  // ...
  const [state, setState] = useSharedState(ExampleStore);
  // ...

  function handleChanges(event) {
    setState(event.currentTarget.value);
  }

  return (
    <>
      <h1>{state.foo}</h1>
      <input onChange={handleChange} />
    </>
  );
}

Upvotes: 1

Estus Flask
Estus Flask

Reputation: 222603

GlobalStore subject is redundant. RxJS observables and React context API both implement pub-sub pattern, there are no benefits in using them together this way. If GlobalStore.subscribe is supposed to be used in children to update the state, this will result in unnecessary tight coupling.

Updating glubal state with new object will result in re-rendering the entire component hierarchy. A common way to avoid performance issues in children is to pick necessary state parts and make them pure components to prevent unnecessary updates:

<Context.Consumer>
  ({ foo: { bar }, setState }) => <PureFoo bar={bar} setState={setState}/>
</Context.Provider>

PureFoo won't be re-rendered on state updates as long as bar and setState are the same.

Upvotes: 0

Related Questions