developit / unistore

🌶 350b / 650b state container with component actions for Preact & React
https://npm.im/unistore
2.86k stars 139 forks source link

useStore Hooks? #136

Open thadeu opened 5 years ago

thadeu commented 5 years ago
/**
 * 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?

thadeu commented 5 years ago
/**
 * 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)
developit commented 5 years ago

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);
developit commented 5 years ago

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 5 years ago

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.

thadeu commented 5 years ago

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?

DonnieWest commented 5 years ago

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

jahredhope commented 5 years ago

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.

yuqianma commented 5 years ago

@jahredhope You would be interested in https://github.com/facebookincubator/redux-react-hook

jahredhope commented 5 years ago

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

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.

dan-lee commented 5 years ago

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

mihar-22 commented 4 years ago

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?

developit commented 4 years ago

@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.

mihar-22 commented 4 years ago

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.

dkourilov commented 4 years ago

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?

mihar-22 commented 4 years ago

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 :)

dan-lee commented 4 years ago

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

khuyennguyen7007 commented 4 years ago

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

danielweck commented 4 years ago

@developit ’s WIP gist: https://gist.github.com/developit/ecd64416d955874c3426d4582045533a

tomByrer commented 4 years ago

@developit ’s WIP gist: https://gist.github.com/developit/ecd64416d955874c3426d4582045533a

Wouldn't install on Win10 for me :/