easy-peasy icon indicating copy to clipboard operation
easy-peasy copied to clipboard

Accessing multiple stores from single component

Open AaronMcCloskey opened this issue 3 years ago • 4 comments

I'm trying to access 2 different stores in a single component, but worry that perhaps the architecture of my app may need to change as easy-peasy may not have this functionality.

I have a GlobalStore

import { createStore } from 'easy-peasy';

const globalModel = {
  menuOpen: false,
  toggleMenu: action((state, payload) => {
    state.menuOpen = payload;
  }),
};

const GlobalStore = createStore(globalModel);
export default GlobalStore;

Just for this example, I'll use a single state and action used in the store to define whether the navigation menu is open or not.

The GlobalStore appears at the top level of my app in my App.js file.

import React from 'react';
import { StoreProvider } from 'easy-peasy';
import GlobalStore from './store/GlobalStore';

const App = () => {
  return (
    <StoreProvider store={GlobalStore}>
    </StoreProvider>
  );
};

export default App;

Now, further down the tree, I have another store SearchStore that dictates which view is active in the component.

import { createStore } from 'easy-peasy';

import { action } from 'easy-peasy';

const searchModel = {
  view: 'filter',
  setView: action((state, payload) => {
    state.view = payload;
  }),
};

const SearchStore = createStore(searchModel);
export default SearchStore;

The issue I have now is that in a component that I need to be able to access both stores to update the view with the setView action in the SearchStore and get the value of menuOpen from the GlobalStore but cannot access both concurrently.

The example I have in a component is that I have a styled component that when clicked calls the action setView but its position is also defined by whether the menuOpen is true or not. but obviously, if I try and get the state of menuOpen it will be undefined as it does not exist in SearchStore

const Close = styled.span`  
  $(({ menuOpen }) => menuOpen ? `
   // styles go here
  ` : `` }
`;

const setView = useStoreActions((action) => action.setView);
const menuOpen = useStoreState((state) => state.menuOpen);

<Close menuOpen={menuOpen} onClick={() => setView('list')}>

Is this possible? Any help would be much appreciated.

AaronMcCloskey avatar Feb 01 '21 18:02 AaronMcCloskey

By no means is this post an official solution, but I wanted to try to help nonetheless.

How I have managed global and subset states is to create a nested store.

import globalModel from './globalModel';
import searchModel from './searchModel';

const store = createStore({
 ...globalModel,
 search: searchMode,
});

// CloseIcon
const setView = useStoreActions((actions) => actions.search.setView);
const menuOpen = useStoreState((state) => state.menuOpen);

I have done this nested store approach several times and have found it immensely convenient. I've had a similar store setup like this before:

const store = createStore({
 settings: settingsModel, // global-like values like language and theme or menuOpen in your case
 products: productsModel,
 cart: cartModel,
 ...

But if that's not what you're looking for, something else worth considering is store-specific hooks.

import { createTypedHooks } from 'easy-peasy';

const typedGlobalHooks = createTypedHooks<GlobalModel>();
export const useGlobalActions = typedGlobalHooks.useStoreActions;
export const useGlobalState = typedGlobalHooks.useStoreState;

const typedSearchHooks = createTypedHooks<SearchModel>();
export const useSearchActions = typedSearchHooks.useStoreActions;
export const useSearchState = typedSearchHooks.useStoreState;

// CloseIcon
import { useSearchActions, useGlobalState } from './store';

const setView = useSearchActions(actions => actions.setView); 
const menuOpen = useGlobalState(state => state.menuOpen);

I'm not sure how this approach works without TypeScript, however.

I hope this helps :)

yuschick avatar Feb 17 '21 11:02 yuschick

You can put multiple models in a store like this:

import { createStore } from 'easy-peasy';

const menuModel = {
  menuOpen: false,
  toggleMenu: action((state, payload) => {
    state.menuOpen = payload;
  }),
};

const searchModel = {
  view: 'filter',
  setView: action((state, payload) => {
    state.view = payload;
  }),
};

const globalModel = {
  menu: menuModel,
  search: searchModel,
}

const GlobalStore = createStore(globalModel);
export default GlobalStore;

or you can create multiple stores using createContextStore API

duygiangdg avatar Feb 20 '21 10:02 duygiangdg

Thanks, @yuschick & @duygiangdg these approaches have helped.

I guess my next question is that I currently have my StoreProvider at the top level of my application, wrapping over all my components. Adding an inner StoreProvider in one my components, overrides my global store, which I do need to access on all of my routes.

Say I have a route, I have a store model specific for it, that page has and id param in the URL and the store state gets updated with data based on that id, if I were to navigate away from that route and select the same route again with a different id in the URL param, the previous store state will display until it's overwritten.

Which means, on the unmount of each route that updates the store in any way I have to set the state back to default? That way you don't see the previously accessed id data on a route a user has accessed before.

I can maybe do a CodeSanbox later to detail this a bit better.

Is there a better way around this? That would allow me to do something like the following or any way I don't have to remember to update the state on the unmount of certain components.

<StoreProvider store={globalStore}>
   <SomeComponent />
   <OtherComponent>
     <StoreProvider store={otherStore}>
       <ComponentUsingOtherStore>
           /* 
             Can access global store but also otherStore
             other store native to ComponentUsingOtherStore component
             meaning navigating away from screen removes store values 
             so state is clean when entering ComponentUsingOtherStore again
           */
       </ComponentUsingOtherStore>
     </StoreProvider>
   </OtherComponent>
</StoreProvider>

AaronMcCloskey avatar Feb 23 '21 16:02 AaronMcCloskey

I think the answer to this is to use the createContextStore api. That will allow you to scope things in a very fine grained way, while creating separate contexts that don't clash.

damassi avatar Jun 15 '21 04:06 damassi