dai-shi / react-tracked

State usage tracking with Proxies. Optimize re-renders for useState/useReducer, React Redux, Zustand and others.
https://react-tracked.js.org
MIT License
2.73k stars 72 forks source link

Is it possible to use state outside of the React context? #33

Closed lukejagodzinski closed 4 years ago

lukejagodzinski commented 4 years ago

In Redux, I can do something like:

import store from "./store";

store.dispatch({ type: "SOME_ACTION" });
const state = store.getState();

in whatever place I want. When using React Context, I'm kinda forced to use it only from within the component. There are ways of creating functions that get reference to dispatch function but still everything has to be done in the context of Provider.

I would like to use react-tracked but I guess it's currently impossible to switch from Redux when someone is using store outside of the React context? Right?

dai-shi commented 4 years ago

Yeah, good point. That's one of the biggest design differences from Redux. React Tracked does just use React state. This is very important to support Concurrent Mode. (Even my other lib react-hooks-global-state isn't fully CM compatible, because it allows Redux-style dispatch.)

I'm pretty sure we use useEffect for your requirement. Could you tell me your use case as simple as possible? And, I'd create a new example to show the use case.

lukejagodzinski commented 4 years ago

@dai-shi so the use case is that I have the React app in the iframe on the host website. I collect events from the host website and send them down to React app. But all the initialization happens in the React app. One example could be:

import store from "./store";

function handleHostEvents(hostWindow: Window) {
  hostWindow.document.addEventListener("click", () => {
    store.dispatch({ type: "INCREMENT_CLICKS" });
  });
}

where the handleHostEvents is being invoked outside of the React context.

The other example which is probably antipattern is to add some value from the state on each HTTP request.

import store from "./store";

async function makeRequest(url: string, data: any) {
  const state = store.getState();
  await fetch(url, {
    body: JSON.stringify({ ...data, clicksCount: state.clicksCount });
  });
}

any hint how to deal with that would be appreciated :). Thank you!

dai-shi commented 4 years ago

@lukejagodzinski Here you go:

addEventListener

This one is probably easy. Here's an example.

import { useReducer, useEffect } from 'react';
import { createContainer } from 'react-tracked';

const initialState = ...;
const reducer = ...;

const useValue = ({ hostWindow }) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  useEffect(() => {
    const listener = () => {
      dispatch({ type: "INCREMENT_CLICKS" });
    };
    hostWindow.document.addEventListener("click", listener);
    return () => {
      hostWindow.document.removeEventListener("click", listener);
    };
  }, [hostWindow]);
  return [state, dispatch];
};

const {
  Provider,
  useTrackedState,
  useSelector,
  useUpdate: useDispatch,
} = createContainer(useValue);

const App = () => (
  <Provider hostWindow={...}>
    ...
  </Provider>
);

If hostWindow is globally accessible, you might not need to pass it as props.

I'd probably put something like this in Recipes.

makeRequest

It depends on where you invoke makeRequest.

The current React Tracked tutorial uses use-reducer-async. In this case, you can getState. It should work in usual cases, but there can be edge cases that need some consideration in the future.

lukejagodzinski commented 4 years ago

Hmm the addEventListener example is actually quite elegant and I could create my custom hook to do all of the events handling in the separate file. I will try that, thanks! :) and the hostWindow is just injected dependency accessible globally, so yes I will not have to pass it as the prop.

About the makeRequest, it's actually a helper function that I've created that is being used like async action but without using async actions. It's probably anti pattern but I was switching back and forth between React Context and Redux back then and settled on this working but not pretty solution. But as you said I could probably access state just in the async action :). Thanks for the help!

And yes it's definitely worth adding such an example to Recipes :)

dai-shi commented 4 years ago

You are welcome. Hope you like it!

dai-shi commented 4 years ago

Added: https://react-tracked.js.org/docs/recipes/#usereducer-with-event-listener