Easy-peasy: Local stores in components

Created on 21 Mar 2019  路  14Comments  路  Source: ctrlplusb/easy-peasy

I love how much easy-peasy simplifies global stores and I'd like to get same experience inside components. Very often I'd do HTTP request inside component and not share this data with other components. In past I used to solve this with simple MobX model, that would accept url, make request and set .result property on sucess or .error on failure. I would then instantiate this model in component and use it.

Easy-peasy models behave very much the same but I don't see a way to create local store, I can only use one provided by StoreProvider. Is it possible to create local store inside one component and have this component responding to store changes?

馃Аhelp wanted 馃А

Most helpful comment

I had a relatively similar idea a couple of days ago and came up with an example of an API that would look a lot like how we interact with easy-peasy for the global store right now.

// for example purposes
function thunk(lambda): any {
  return undefined;
}
function action(lambda): any {
  return undefined;
}
function useLocalEasyPeasy(model: any, injections: any): any {
  return undefined;
}


// REAL CODE STARTS HERE 
const injections = {
  fetch: fetch
};
const MyComponent: React.FC<{}> = props => {
  // we could also extract the internals of the hooks for testing purposes
  const [state, dispatch] = useLocalEasyPeasy(
    {
      userName: "",
      setUserName: action((state, payload) => {
        return {
          ...state,
          userName: payload.userName
        };
      }),
      getUserNameFromServer: thunk(
        async (actions, getState, payload, helpers) => {
          const userName = await helpers.injections.fetch("/api/getUserName");

          actions.setUserName({ userName: userName });
        }
      )
    },
    injections
  );

  return (
    <div>
      <div>{state.userName}</div>

      <div>
        <button
          onClick={() => {
            dispatch.getUserNameFromServer();
          }}
        >
          Get User Name
        </button>
      </div>
    </div>
  );
};

All 14 comments

I had a relatively similar idea a couple of days ago and came up with an example of an API that would look a lot like how we interact with easy-peasy for the global store right now.

// for example purposes
function thunk(lambda): any {
  return undefined;
}
function action(lambda): any {
  return undefined;
}
function useLocalEasyPeasy(model: any, injections: any): any {
  return undefined;
}


// REAL CODE STARTS HERE 
const injections = {
  fetch: fetch
};
const MyComponent: React.FC<{}> = props => {
  // we could also extract the internals of the hooks for testing purposes
  const [state, dispatch] = useLocalEasyPeasy(
    {
      userName: "",
      setUserName: action((state, payload) => {
        return {
          ...state,
          userName: payload.userName
        };
      }),
      getUserNameFromServer: thunk(
        async (actions, getState, payload, helpers) => {
          const userName = await helpers.injections.fetch("/api/getUserName");

          actions.setUserName({ userName: userName });
        }
      )
    },
    injections
  );

  return (
    <div>
      <div>{state.userName}</div>

      <div>
        <button
          onClick={() => {
            dispatch.getUserNameFromServer();
          }}
        >
          Get User Name
        </button>
      </div>
    </div>
  );
};

This would certainly be an interesting idea to experiment with. I like the general direction of your API @RPDeshaies 馃憤

How about like this:

const [useCounter] = createStore({
  count: 0,
  increment: action(state => state + 1),
  decrement: action(state => state -1),

  // effect
  incrementAsync: thunk(async (actions) => {
    await sleep(1000);
    actions.incrment();
  })
})

Thus we can use useCounter directly in component to get a local store.

function CounterDisplay() {
  const counter = useCounter()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <span>{counter.count}</span>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

For cross components communication, we can use something like unstated-next to create a context to get a shared store.

const Counter = createContainer(useCounter)

function CounterDisplay() {
  const counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <span>{counter.count}</span>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}

function App() {
  return (
    <Counter.Provider>
      <CounterDisplay />
      <CounterDisplay />
    </Counter.Provider>
  )
}

Hey @littlehaker

I really like this API. A lot of shared ideas with that of @RPDeshaies, but the container addition inspired by unstated-next is really cool. This introduces a whole new level of store portability. 鉂わ笍

I would just recommend we make a couple of minor adjustments though; to split the state/actions, and to keep the tuple return type consistent between the standard/container versions.

i.e.

const [state, actions] = useCounter();
const [state, actions] = Counter.useContainer();

馃憤

Really great though. Would you consider starting a PR for this?

Hi @ctrlplusb,

Thanks for your appreciation. I will try making a PR but not guaranteed.

Hey @littlehaker - no problem. If you do start one then please give a heads up in case myself or someone else picks it up. Anyone welcome to this though 馃憤

Hey @ctrlplusb, I've made a demo here. Please take a look. Basically, everything goes very well!

Awesome, @littlehaker - this is a great reference!

There are some interesting complexities and considerations to make around these APIs. I've got a few solid ideas. Will hopefully have time this weekend to create a first parse implementation.

Hi all;

I have started to create some prototypes along with basic docs. I still need to flesh this out far more, and include actual API docs. I'd like quite a few more examples covering various use cases.

Feel free to have a look. Early feedback is welcome!

https://codesandbox.io/s/easy-peasy-local-container-store-prototype-209xd

The container store API still needs more work. I plan to let it support initial state similar to the local stores. Also I want to expose useState and useActions hooks for the container store, which would work similar to standard global easy peasy store hooks. Having these in addition the useStore hook that is on the containers provides an additional optimisation when a container store is fairly complex in structure and/or is subject to many updates. Being able to focus on the parts consumers care about avoids unnecessary rerenders.

Hi @ctrlplusb, great work, so would useState and useActions be like below?

const count = Counter.useState(state => state.count);
const inc = Counter.useActions(actions => actions.inc)

Hey @littlehaker - thanks.

Yep, the container version will have useState, useActions, and useDispatch hooks available in the full implementation. 馃憤

@ctrlplusb, Bravo! Looking forward to this version.

Oh yeah, forgot to close this. 馃槢

The docs for these helpers are in the extended API section on the new website.

Was this page helpful?
0 / 5 - 0 ratings