yamalight / outstated

Simple hooks-based state management for React
107 stars 7 forks source link

Docs: How to useStore from another store? #9

Closed filipjnc closed 5 years ago

filipjnc commented 5 years ago

Hi, how would you create selectors in a store with values from another store?

When I put useStore in another store I get an error that it should be wrapped in a <Provider>.

Thanks!

yamalight commented 5 years ago

@filipjnc can you please provide a test project that demonstrates what you want to achieve?

filipjnc commented 5 years ago

Here you go: https://stackblitz.com/edit/react-7nrqcn

The error happens when I construct the selector in store2.js:

const { posts } = useStore(store1);

The example makes very little sense, but in a larger application the stores must interact to derive state (like in Reselect for Redux). Thanks for any tips how to get this working without unexpected side-effects!

yamalight commented 5 years ago

@filipjnc this unfortunately won't work with the current way outstated is written. useStore relies on existence of StateContext - and this is something that's not present during init.

It should be possible to rewrite outstated Provider to account for that, but I'm not sure it's worth it. I've been thinking about the case of accessing one store from another and to be honest I don't see any valid cases so far. E.g. for the example you gave - IMO the second store should be a part of the first once since they are logically related. If you have more convincing examples - would love to see them.

filipjnc commented 5 years ago

Say you have a store where you persist raw data coming in:

posts: {
  1: {
     "id": 1,
     "title": "Post 1"
  },
  2: {
     "id": 2,
     "title": "Post 2"
  },
}

Then in another store you have stuff like activePostId: 1 based on user interaction. I need a computed/derived property activePost = activePostId ? posts[activePostId] : null, which is shared across different components.

Maybe it is possible to create a hook that provides the user the state (or a slice of it) with the current values in it? Easy Peasy does something similar: https://easy-peasy.now.sh/docs/tutorial/accessing-state.html

yamalight commented 5 years ago

Why would activePostId be in a different store? That's what I don't quite understand. The way it makes sense to structure this to me is:

import { useState } from 'react';

const store = () => {
  const [activePostIndex, setActivePostIndex] = useState(0);
  const [posts, setPosts] = useState([
    'Post 1',
    'Post 2',
    'Post 3'
  ]);
  const activePost = useMemo(
    () => posts[activePostIndex], 
    [posts, activePostIndex]
  );

  return { activePostIndex, setActivePostIndex, posts, setPosts };
};

export default store;

What's the point of splitting it? 🤔

filipjnc commented 5 years ago

I guess I will have to merge more stores. It was just more readable when I splitted the data store from the one holding user interaction stuff. Thanks for your insights!

yamalight commented 5 years ago

@filipjnc if you really want to use multiple stores, I'd encourage you to explore alternative solution, e.g. you could do this with unstated-next - like so

filipjnc commented 5 years ago

@yamalight I don't like unstated-next for its container callback hell. There are other solutions like easy-peasy but I prefer the redux architecture for much larger apps.

A good solution for outstated that I found in the meantime is to create custom react hooks for these composed selectors:

function useActiveFile() {
  const { activeFileIndex } = useStore(uiStore);
  const { files } = useStore(dataStore);
  const activeFile = useMemo(() => files[activeFileIndex], [files, activeFileIndex]);

  return { activeFile };
}

Maybe you could include this tip in the docs for those who initially feel lost about composed selectors and tend to forget what React offers out of the box :)

yamalight commented 5 years ago

@filipjnc this looks pretty neat! would appreciate a PR with this :)