developit/unistore

useStore Hooks?

thadeu opened this issue ยท 19 comments

/**
 * Available methods
 *
 * value
 * isValue
 * setValue({ item: value })
 * withValue({ item: value })
 *
 * @param {*} key
 */
export function useStore(key) {
  let camelKey = _.camelCase(key)
  let setKey = _.camelCase(`set_${key}`)
  let isKey = _.camelCase(`is_${key}`)
  let withKey = _.camelCase(`with_${key}`)

  let selected = store.getState()[camelKey]

  function setter(value) {
    if (typeof value === 'function') {
      const newValue = value(selected)

      return store.setState({
        [camelKey]: { ...selected, ...newValue }
      })
    }

    return store.setState({
      [camelKey]: { ...selected, ...value }
    })
  }

  let getter = selected

  return {
    [camelKey]: getter,
    [isKey]: getter,
    [setKey]: setter,
    [withKey]: setter
  }
}

Used

import { useStore } from 'unistore'

export function disconnected() {
  log('Ably Events | event: disconnected')

  const { setAblySocket } = useStore('ablySocket')
  setAblySocket({ status: 'disconnected' })

  const { emitter } = useStore('emitter')
  emitter.emit('ably', { payload: 'disconnected' })
}

@developit what do you think about this?

/**
 * const [loggedIn, setLoggedIn] = useHookStore('loggedIn')
 * console.log(loggedIn)
 * @param {*} props
 */
export function useHookStore(prop) {
  const selected = store.getState()[prop]
  const setter = value => store.setState({ [prop]: { ...selected, ...value } })

  return [selected, setter]
}

example:

import { useHookStore } from 'unistore'

const [ablySocket, setAblySocket] = useHookStore('ablySocket')
console.log(ablySocket)

I like it! It'd be a little unfortunate to drop the action binding stuff though. Maybe something like this?

import { createContext } from 'preact';
import { useState, useContext, useMemo, useEffect } from 'preact/hooks';

const StoreContext = createContext(store);
export const Provider = StoreContext.Provider;

function runReducer(state, reducer) {
  if (typeof reducer==='function') return reducer(state);
  const out = {};
  if (Array.isArray(reducer)) for (let i of reducer) out[i] = state[i];
  else if (reducer) for (let i in reducer) out[i] = state[reducer[i]];
  return out;
}

function bindActions(store, actions) {
  if (typeof actions=='function') actions = actions(store);
  const bound = {};
  for (let i in actions) bound[i] = store.action(actions[i]);
  return bound;
}

export function useStore(reducer, actions) {
  const { store } = useContext(StoreContext);
  const [state, set] = useState(runReducer(store.getState(), reducer));
  useEffect(() => store.subscribe(state => {
      set(runReducer(state, reducer));
  }));
  const boundActions = useMemo(bindActions, [store, actions]);
  return [state, boundActions];
}

Usage:

import { Provider, useStore } from 'unistore/preact/hooks';

const ACTIONS = {
  add(state) {
    return { count: state.count + 1 };
  }
};

function Demo() {
  const [state, actions] = useStore(['count'], ACTIONS);
  return <button onclick={actions.add}>{state.count}</button>
}

render(<Provider value={store}><Demo /></Provider>, root);

Another neat alternative would be to split the hooks into useStoreState() and useActions():

const ACTIONS = {
  add: ({ count }) => ({ count: count + 1 })
};

function Demo() {
  const [count] = useStoreState(['count']);
  const add = useActions(ACTIONS.add);
  // or to bind them all:
  const { add } = useActions(ACTIONS);
  return <button onclick={add}>{count}</button>
}
dy commented

That pattern would save some levels of nested components. @developit any plans on creating them? I'm right in the situation of refactoring App with hooks, that'd be a great help. I can come up with PR I guess.

I like it! It'd be a little unfortunate to drop the action binding stuff though. Maybe something like this?

import { createContext } from 'preact';
import { useState, useContext, useMemo, useEffect } from 'preact/hooks';

const StoreContext = createContext(store);
export const Provider = StoreContext.Provider;

function runReducer(state, reducer) {
  if (typeof reducer==='function') return reducer(state);
  const out = {};
  if (Array.isArray(reducer)) for (let i of reducer) out[i] = state[i];
  else if (reducer) for (let i in reducer) out[i] = state[reducer[i]];
  return out;
}

function bindActions(store, actions) {
  if (typeof actions=='function') actions = actions(store);
  const bound = {};
  for (let i in actions) bound[i] = store.action(actions[i]);
  return bound;
}

export function useStore(reducer, actions) {
  const { store } = useContext(StoreContext);
  const [state, set] = useState(runReducer(store.getState(), reducer));
  useEffect(() => store.subscribe(state => {
      set(runReducer(state, reducer));
  }));
  const boundActions = useMemo(bindActions, [store, actions]);
  return [state, boundActions];
}

Usage:

import { Provider, useStore } from 'unistore/preact/hooks';

const ACTIONS = {
  add(state) {
    return { count: state.count + 1 };
  }
};

function Demo() {
  const [state, actions] = useStore(['count'], ACTIONS);
  return <button onclick={actions.add}>{state.count}</button>
}

render(<Provider value={store}><Demo /></Provider>, root);

That pattern wouldn't support to preact 8.x right? so, we should be find solution for all versions, or something that work to separated versions.

@developit you have any ideia about it?

I was playing around with this tonight and came up with #153

I still can't figure out why I have ONE test failing, but it retains all the previous APIs while also giving us a hook and nearly all previous tests passing. Heavily inspired by @thadeu to make something that will feel familiar to anyone who used the old API

Only downfall is that it will be a breaking change since it uses the new Context API

Feedback welcome

I'd like to add a suggestion about the API.

Pre React Hooks it was typical to use a single connect command for a component, with multiple selectors and multiple actions per connect.

I'm not sure this behaviour should carry over to hooks. Consumers can now use multiple hooks in a component. And this could be made possible by separating out selecting content from the store, useSelector, and creating an action, useAction.

With connect statement you might use something like:

const mapStateToProps = state => ({user: getUserInfo(state), books: getBooks(state)})
const actions = [addBook, removeBook]
connect(mapStateToProps, actions)

With Hooks you'd be able to pull these out to seperate commands:

const userInfo = useSelector(selectUserInfo)
const books = useSelector(selectBooks)
const add = useAction(addBook)
const remove = useAction(removeBook)

This potentially allows you to pull out common hooks such as useSelectUserInfo that might be used in multiple places.

Exactly @yuqianma . Aligning the API with Redux will help people moving between the frameworks.
The only difference is what consumers are passing to useAction.

I had a crack at implementing it when I commented above, just a WIP:
https://gist.github.com/jahredhope/c0d490ec2c58aa45efd11d138b72d9ff

  • Issues with the connect statement causing re-renders on irrelevant state changes, I think I need to work on the equality comparison.
  • useSelector doesn't accept an equality function. Wanted to get an MVP working first.

Though the big win for me is the TypeScript type inference.
State param's type is inferred in your actions:
Screen Shot 2019-07-07 at 9 16 58 am
And actions automatically have their parameters Types inferred:
Screen Shot 2019-07-07 at 9 13 35 am

Update:
Working on an implementation here:
https://github.com/developit/unistore/compare/master...jahredhope:add-hooks?expand=1
Which comes from a standalone implementation here:
https://github.com/jahredhope/react-unistore
I've just been testing the standalone implementation in some apps and seems to be working well.

Would be lovely to get this in. I love this little library, but it's definitely missing support for hooks

I created some hooks for Unistore based heavily on react-redux and redux-zero.

Repo: https://github.com/mihar-22/preact-hooks-unistore

I'd love some feedback and maybe integrate it into Unistore?

@mihar-22 looks really good. The only change I'd like to see in your implementation is to allow passing selector values to useSelector() rather than just functions. You can import { select } from 'unistore/src/utils.js' or just copy it over.

All done @developit. I can keep it as a separate package or I can make a PR to pull it in and we can add it to src/hooks? Either way no problemo.

Hey @mihar-22, I was following this thread and tried to use your proposal in my typical dashboard app that I'm porting from another framework.

Apparently, I don't see how useActions hook must be used with async functions. The bound value gets resolved to a listener only when the modified state is returned from hook function. Do you have any recommended usage pattern in mind?

Hey @dkourilov, if you can raise the issue over at https://github.com/mihar-22/preact-hooks-unistore then I can try and help when I have a moment. If you can provide a little bit more information that'd help, thanks :)

Are there any plans to go forward with official unistore react/preact hooks?

Hope to see unistore and preact-hooks-unistore in a united package soon.

@developit โ€™s WIP gist: https://gist.github.com/developit/ecd64416d955874c3426d4582045533a

Wouldn't install on Win10 for me :/