1

I have a react context which is being used as a provider across multiple components.

I want to modify that state and update the UI from a normal method totally outside of React components. ie NOT a functional component or class, just a normal piece of JS code.

From what I can see the provider/context can only be accessed from within a render method or React.FC

I can pass a function in but it seems only from some type of React component that is sandwiched between <Context.Provider>

and useContext only lights up once the relevant item has been passed into a render loop.

Is there a way I can create some type of store that I can just call a setState() method on its data, but where such updates would be reactive in the UI?

[edit] The reason I want this is cos I have an external API call that gives me a long running callback. I don't see a need to wrap that API up in all kinds of react stuff as I want that module to be portable (eg to server side) and just interact with app state to update the UI display.

dcsan
  • 11,333
  • 15
  • 77
  • 118
  • Can you explain what you want to do with this? This would make it easier to find a solution. Personally I don´t think it is possible without any hacks. – user148397 Nov 02 '19 at 21:59
  • added @user148397 hope that clarifies a bit – dcsan Nov 02 '19 at 22:12
  • so one way seems to be to `register` the context object with the Lib/Api and then call setState on that object. It updates the UI. Maybe this is 'inversion of control', it certainly feels a bit backward :D – dcsan Nov 02 '19 at 22:25
  • Thanks, I think there is no other option to call the api somewhere in react, to save the values in the context. But you can wrap your api in a custom wrapper and cache the api results for e.g. GET requests. Then you can call your api somewhere else earlier and then trigger the same function again to get the results from the cache if the request is finished or trigger a new request. – user148397 Nov 02 '19 at 22:28
  • You can write a custom store like redux and dispatch actions to the store, but just to trigger api requests somewhere else I am not sure if this is the right choice in this case – user148397 Nov 02 '19 at 22:32

2 Answers2

1

As far as I can tell you still need to opt into the React API if you wanna build the UI with React.

Here what you need to do:

  • Need a context to share the value across your component tree
  • Need a Provider component at the root
  • Need to subscribe to the external API to receive the new values and set that to your context
  • Use the context consumer to get the shared value

Here is an example how this could work

Amin Paks
  • 276
  • 2
  • 15
  • thanks. I want to use the react API in the UI components that display, but not in completely non-react based API adapter code. However those adapters do receive data that I need to display. But it seems odd to wrap adapters somehow in a `render()` of another UI component just to connect them. I really just want an exposed method to modify the state of data in the UI tree (without all the overhead of redux/actions/reducers etc). In your example you give the adapter a callback to call and update the react tree... So the UI is subscribing to `updateState` and other React magic methods. – dcsan Nov 04 '19 at 00:13
  • Yeah. This example is the simplest and align with the latest React updates. – Amin Paks Nov 04 '19 at 00:19
1

I assume you are asking this because you don't use a state manager like redux where you could dispatch an action from anywhere to reset state.

One option; as you already mentioned; is to create your own store and store provider. Maybe something like this will work for you:

const store = (initialState => {
  let value = initialState;
  let listeners = [];
  const getState = () => value;
  const setState = fn => {
    value = fn(value);
    listeners.forEach(l => l(value));
  };
  const subscribe = listener => {
    listeners.push(listener);
    return () =>
      (listeners = listeners.filter(f => f !== listener));
  };
  return { getState, setState, subscribe };
})({ counter: 1 }); //pass initial state to IIFE

const Store = React.createContext();

function Provider({ store, children }) {
  const [state, setState] = React.useState(
    store.getState()
  );
  React.useEffect(
    () =>
      store.subscribe(() => {
        const lastState = store.getState();
        //if a lot of setState calls are made synchronously
        //  do not update dom but let it batch update state
        //  before triggering a render
        Promise.resolve().then(() => {
          if (lastState === store.getState()) {
            setState(store.getState());
          }
        });
      }),
    [store]
  );
  return (
    <Store.Provider value={state}>
      {children}
    </Store.Provider>
  );
}

function App() {
  const state = React.useContext(Store);
  return <div>{state.counter}</div>;
}

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

//call store setState from anywhere
setInterval(
  () =>
    store.setState(state => ({
      ...state,
      counter: state.counter + 1,
    })),
  1000
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
HMR
  • 37,593
  • 24
  • 91
  • 160
  • Is that the minimum implementation of a store? I'm not clear on `l(value)` - you are calling a listener, which is a function, with the state of the store? yet `subscribe` is where you add listener (callback anon function?), and those functions themselves call `setState` on the store... I guess i'd have to unravel this code, but perhaps the conclusion is that there is no clean way to get a react banana without hacking through the whole jungle. – dcsan Nov 04 '19 at 00:29
  • @dcsan l is a listener that is added with subscribe. The only listener is added in provider and will set it's own state when store state changes so it causes a re render. Provider also puts that state in it's context. It's how redux works but without dispatch and reducers. – HMR Nov 04 '19 at 06:26