Comment by Capricorn2481

23 days ago

I'm all for people avoiding React if they want, but I do want to respond to some of this, as someone who has made a few React apps for work.

> When they started adding new hooks just to work around their own broken component/rendering lifecycle, I knew React was doomed to become a bloated mess.

Hooks didn't fundamentally change anything. They are ways to escape the render loop, which class components already had.

> Nobody in their right mind is remembering to use `useDeferredValue` or `useEffectEvent` for their very niche uses.

Maybe because you don't necessarily need to. But for what it's worth, I'm on old versions of React when these weren't things, and I've built entire SPAs out without them at work. But reading their docs, they seem fine?

> And don't get me started on React's sad excuse for global state management with Contexts. A performance nightmare full of entire tree rerenders on every state change

I think it's good to give context on what a rerender is. It's not the same as repainting the DOM, or even in the same order of magnitude of CPU cycles. Your entire site could rerender from a text input, but you're unlikely to notice it even with 10x CPU slowdown in Devtools, unless you put something expensive in the render cycle for no reason. Indeed, I've seen people do a fetch request every time a text input changes. Meanwhile, if I do the same slowdown on Apple Music which is made in Svelte, it practically crashes.

But pretty much any other state management library will work the way you've described you want.

My issue with React Context is you can only assign initial state through the `value` prop on the provider if you need that initial state to be derived from other hook state/data, which requires yet another wrapper component to pull those in.

Even if you make a `createProvider` factory to initialize a `useMyContext` hook, it still requires what I mentioned above.

Compare this to Vue's Pinia library where you can simply create a global (setup) hook that allows you to bring in other hooks and dependencies, and return the final, global state. Then when you use it, it points to a global instance instead of creating unique instances for each hook used.

Example (React cannot do this, not without enormous boilerplate and TypeScript spaghetti -- good luck!):

  export const useMyStore = defineStore("myStore", () => {
    const numericState = ref(0);
    const computedState = computed(() => reactiveState.value * 2);
    const numberFromAnotherHook = useAnotherHook();
    
    const { data, error, loading } = useFetch(...);

    const derivedAsyncState = computed(() => {
      return data.value + numericState.value + numberFromAnotherHook.value;
    });

    return {
      numericState,
      computedState,
      numberFromAnotherHook,
      derivedAsyncState,
    }
  });

This is remarkably easy, and the best part is: I don't have to wrap my components with another <Context.Provider> component. I can... just use the hook! I sorely wish React offered a better way to wire up global or shared state like this. React doesn't even have a plugin system that would allow someone to port Pinia over to React. It's baffling.

Every other 3rd party state management library has to use React Context to initialize store data based on other React-based state/data. Without Context, you must wait for a full render cycle and assign the state using `useEffect`, causing your components to flash or delay rendering before the store's ready.

  • You can use Tanstack Query or Zustand for this in React. They essentially have a global state, and you can attach reactive "views" to it. They also provide ways to delay rendering until you have the data ready.

    Your example would look like:

      const id = useState(...);
      const numericState = useState(0);
      
      const q = useQuery({
          queryFn: async (context) => {
            const data = await fetch(..., {signal: context.signal});
            const derivedState = numericState + data.something;
            return `This is it ${derivedState}`;
          },
          queryKey: ["someState", id],
        }, [id, numericState]);
      ...
    
      if (q.isLoading) {
        return <div>loading...</div>;
      }
    
      return <div>{q.data}</div>;
    
    

    It'll handle cancellation if your state changes while the query is being evaluated, you can add deferred rendering, and so on. You can even hook it into Suspense and have "transparent" handling of in-progress queries.

    The downside is that mutations also need to be handled by these libraries, so it essentially becomes isomorphic to Solid's signals.

    • I've used React Query and Zustand extensively in my projects, and unfortunately Zustand suffers from the same issue in cases where you aren't dealing with async data. I'm talking about React state + data that's already available, but can't be used to initialize your store before the first render cycle.

      Here's how Zustand gets around this, and lo-and-behold: it requires React Context :( [1] (Look at how much boilerplate is required!)

      React Query at least gives you an `initialData` option [2] to populate the cache before anything is done, and it works similarly to `useState`'s initializer. The key nuance with `const [state, setState] = useState(myInitialValue)` is the initial value is set on `state` before anything renders, so you don't need to wait while the component flashes `null` or a loading state. Whatever you need it to be is there immediately, helping UIs feel faster. It's a minor detail, but it makes a big difference when you're working with more complex dependencies.

      1. https://zustand.docs.pmnd.rs/guides/initialize-state-with-pr...

      2. https://tanstack.com/query/v5/docs/framework/react/guides/in...

      ---

      I guess I could abuse React Query like this...

        function useGlobalStore() {
          const myHookState = useMyHook(); // not async
      
          return useQuery({
            initialData: {
              optionA: true,
              optionB: myHookState,
            }
          });
        }
      

      And you'd have to use `queryClient` to mutate the state locally since we aren't dealing with server data here.

      But here's what I really want from the React team...

        // Hook uses global instance instead of creating a new one each time it's used. No React Context boilerplate or ceremony. No wrapping components with more messy JSX. I can set state using React's primitives instead of messing with a 3rd party store:
      
        const useGlobalStore = createGlobalStore(() => {
          const dataFromAnotherHook = useAnotherHook();
      
          const [settings, setSettings] = useState({
            optionA: true,
            optionB: dataFromAnotherHook,
          });
      
          return {
            settings,
            setSettings,
          }
        });

      2 replies →