Giter VIP home page Giter VIP logo

Comments (14)

dai-shi avatar dai-shi commented on July 29, 2024 1

@jacob-ebey Yeah, my original motivation and implementation was also eliminating context, and then eventually I came back to use context which seems to work better in concurrent mode in the future. I also wanted to make use of observedBits for performance.

from react-hooks-global-state.

dai-shi avatar dai-shi commented on July 29, 2024

Hi!
This is not exhaustive pros and cons, but let me explain.

If we go with the first API design, the dispatch is globally available in a file and you can define action functions outside of components. You can even define them in a separate file and import them. We don't need mapDispatchToProps technique like in react-redux.

const initialState = { counter: 0 }; // initialState is not optional.
const { GlobalStateProvider, dispatch, useGlobalState } = createStore(reducer, initialState);

const increment = () => dispatch({ type: 'increment' });
const decrement = () => dispatch({ type: 'decrement' });

const Counter = () => {
  const [value] = useGlobalState('counter');
  return (
    <div>
      <span>Counter: {value}</span>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
    </div>
  );
};

Not much different if use button here, but suppose we use a custom MyButton extending React.PureComponent or with React.memo(), we probably want onClick function identity.
To keep the function identity with the second API design, we need to use useCallback().

import { useCallback } from 'react';

const initialState = { counter: 0 }; // initialState is not optional.
const { GlobalStateProvider, useGlobalState } = createStore(reducer, initialState);

const Counter = () => {
  const [value, dispatch] = useGlobalState('counter');
  const increment = useCallback(() => dispatch({ type: 'increment' }), []);
  const decrement = useCallback(() => dispatch({ type: 'decrement' }), []);
  return (
    <div>
      <span>Counter: {value}</span>
      <MyButton onClick={increment}>+1</MyButton>
      <MyButton onClick={decrement}>-1</MyButton>
    </div>
  );
};

If developers are used to memoization, this is trivial, but it may still introduce boilerplate code, which I would like to avoid.

The downside of the global dispatch is that you need to mock it when testing.
However, we still have useGlobalState globally and probably it won't change much.
There might be other differences, and I'd admit that I need to learn more about testing components with hooks.


Historically, this library was developed primarily for the setState style.

  const [value, update] = useGlobalState('counter');

So, the second value of useGlobalState is an update function.
This is still true even if we use the reducer style.
Although I don't recommend using update together with dispatch, it's technically possible.
(As long as you don't use the Redux DevTools.)

We could implement useGlobalStateDispatch, but that doesn't appeal much, does it?

const initialState = { counter: 0 }; // initialState is not optional.
const { GlobalStateProvider, useGlobalStateDispatch, useGlobalState } = createStore(reducer, initialState);

const Counter = () => {
  const [value] = useGlobalState('counter');
  const dispatch = useGlobalStateDispatch();
  return (
    <div>
      <span>Counter: {value}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
    </div>
  );
};

from react-hooks-global-state.

ivan-kleshnin avatar ivan-kleshnin commented on July 29, 2024

Great explanation, man. Honestly, I never used Redux (more than for a few demo cases), but your explanations make sense to me. Thank you!

One more topic I'd like to discuss, if you don't mind:

Dispatch vs Update

Comparing dispatch and update-based approaches...

With the following:

const Counter = () => {
  const [value, update] = useGlobalState('counter');
  return (
    <div>
      <span>Counter: {value}</span>
      <button onClick={() => update(v => v + 1)}>+1</button>
      <button onClick={() => update(v => v - 1)}>-1</button>
    </div>
  );
};

we have pieces of logic incapsulated in JSX. They may be small enough to ignore or large enough to want to abstract away:

// actions.js
let actions = {
  increment: (v) => v + 1,
  decrement: (v) => v + 1,
}

// components/Counter.js
const Counter = () => {
  const [value, update] = useGlobalState('counter');
  return (
    <div>
      <span>Counter: {value}</span>
      <button onClick={() => update(actions.increment)}>+1</button>
      <button onClick={() => update(actions.decrement)}>-1</button>
    </div>
  );
};
  1. Do you use the above approach yourself?

It may be simplified, if the update function is redesigned to return a callable:

// actions.js
let actions = {
  increment: (v) => v + 1,
  decrement: (v) => v + 1,
}

// components/Counter.js
const Counter = () => {
  const [value, update] = useGlobalState('counter');
  return (
    <div>
      <span>Counter: {value}</span>
      <button onClick={update(actions.increment)}>+1</button>
      <button onClick={update(actions.decrement)}>-1</button>
    </div>
  );
};

function update(fn) {
  setState(fn)
}
// vs
function update(fn) {
  return function () { setState(fn) }
}

or it can be exposed as a third function, I dunno:

const [value, update, update2] = useGlobalState('counter')
  1. The benefit of both useReducer and the action-abstraction (mappers), shown above, is a separation between logic and templates which makes the logic easily testable. Of course, there are multiple approaches and personal prereferences, when it comes to testing. If ones favor E2E or high-level integration tests – maybe they don't this step.

  2. Now if we compare reducers and mappers, I believe the main benefit of dispatch is traceability. You can't make a comparable Redux Devtool analogy with update (enclosed functions arguments are invisible, function names are unreliable when you use currying, etc). The main drawback is the extra complexity.

Would like to know your opinion on 1), 2) and 3).

from react-hooks-global-state.

dai-shi avatar dai-shi commented on July 29, 2024

Dispatch vs Update

In general, if you would like to follow "action-abstraction (mappers)", I'd suggest to use dispatch.

So, I'd use dispatch in this scenario, but still you could use update. In this case, I would do like the following.

// actions.js
let actions = {
  increment: (update) => () => update((v) => v + 1),
  decrement: (update) => () => update((v) => v + 1),
}

// components/Counter.js
const Counter = () => {
  const [value, update] = useGlobalState('counter');
  return (
    <div>
      <span>Counter: {value}</span>
      <button onClick={actions.increment(update)}>+1</button>
      <button onClick={actions.decrement(update)}>-1</button>
    </div>
  );
};

You may not like this approach, though. Hm, maybe I wouldn't take this approach either.

Another option: <button onClick={applyAction(update, actions.increment)}>

Again, if you want to separate actions from components, just using dispatch is natural.

However, we don't know about the best practice about the separation of logic and JSX in our new "hooks" world.

What I could imagine is something like this:

// components/CounterContainer.js
const CounterContainer = () => {
  const [value, update] = useGlobalState('counter');
  const increment = useCallback(() => update(v => v + 1), []);
  const decrement = useCallback(() => update(v => v - 1), []);
  return <ConterPresentation value={value} increment={increment} decrement={decrement} />;
};

// components/CounterPresentation.js
const CounterPresentation = ({ value, increment, decrement }) => (
  <div>
    <span>Counter: {value}</span>
    <button onClick={increment}>+1</button>
    <button onClick={decrement}>-1</button>
  </div>
);

I actually like this one. It rather hides a global state in a container. It's more component-oriented approach. It seems to me to fit more with React philosophy.

Seems like it fits better with you to take the dispatch approach.

Like the previous example, the update approach is to hide updating states in a component, so the mindset is probably the other way around.


function update(fn) {
  return function () { setState(fn) }
}

This doesn't work if you need to take an argument like TextBox.
https://github.com/dai-shi/react-hooks-global-state/blob/master/examples/01_minimal/src/index.js#L28-L39

BTW, have you looked into the examples folder? Feedback appreciated.


Please correct me if I misunderstood some of your questions.

from react-hooks-global-state.

ivan-kleshnin avatar ivan-kleshnin commented on July 29, 2024

I actually like this one. It rather hides a global state in a container. It's more component-oriented approach. It seems to me to fit more with React philosophy.

Hmm, I have to disagree here. In my opinion state changing logic should be as simple and straightforward as possible. A function or an object of functions are fine. Reducer is already slightly overcomplicated and I don't see other benefit than a) being more familiar for Redux users b) being more trace/debug friendly. Component is an overkill i.m.o. To test a component you need a fake DOM (an integration test) which is very slow in comparison to pure unit tests you could have otherwise.

BTW, have you looked into the examples folder? Feedback appreciated.

No but I need to. Thanks for the suggestion – I didn't notice them.

This doesn't work if you need to take an argument like TextBox.

Yeah, but I guess something like this can work:

function update(fn, ...args) {
  return function () { setState(fn.bind(null, args)) }
}

I believe Hyperapp v1 had the API like this, may be wrong though.

from react-hooks-global-state.

dai-shi avatar dai-shi commented on July 29, 2024

Hmm, I have to disagree here.

Fair enough. Testing pure functions is always easier.

Yeah, but I guess something like this can work:

I see what you mean. I think we need arguments for the returning function.
What's good about hooks is that you can extend them by yourself based on primitives.

const { GlobalStateProvider, useGlobalState: useGlobalStateOrig } = createGlobalState(initialState);

function useGlobalState(name) {
  const [state, setState] = useGlobalStateOrig(name);
  function update(fn) {
    return function(...args) { setState(fn(...args)); }
  }
  return [state, update];
}

from react-hooks-global-state.

ivan-kleshnin avatar ivan-kleshnin commented on July 29, 2024

What's good about hooks is that you can extend them by yourself based on primitives.

True story 👍 Need to experiment with that.

Do you plan to go in the direction of lensing:

useGlobalState('person');
// vs
useGlobalState('person.address');
// vs
useGlobalState(lensFrom(['person', 'address', ...]);

And yes, your examples are great! Gonna give this library a serious trial :)

from react-hooks-global-state.

dai-shi avatar dai-shi commented on July 29, 2024

This library is pretty much optimized for top-level selector (by observedBits and type inference).
My recommendation for deep property value selection is something like that in examples/11_deep.

Nevertheless, you could build deep selector based on primitives.
A naive implementation to get a deep value would be:

const useGlobalStateByPath = (path) => {
  path = lodash.toPath(path);
  const name = path.shift();
  const [value] = useGlobalState(name);
  return [lodash.get(value, path)];
};

(You need update too for real lensing.)

from react-hooks-global-state.

jacob-ebey avatar jacob-ebey commented on July 29, 2024

You can accomplish global reducers/state without relying on the Context API or "global" dispatch functions:

https://github.com/jacob-ebey/react-hook-utils

from react-hooks-global-state.

jacob-ebey avatar jacob-ebey commented on July 29, 2024

Can you elaborate on what works better in concurrent mode?

from react-hooks-global-state.

dai-shi avatar dai-shi commented on July 29, 2024

The concurrent mode is still not fixed (it will change), and there could still be something I misunderstand. Anyway, here's a reference:
https://blog.isquaredsoftware.com/2018/11/react-redux-history-implementation/

In v6:

The Redux store state is put into an instance of the new createContext API
There is only one store subscriber: the component
This has all kinds of ripple effects across the implementation.

It's fair to ask why we chose to change this aspect. We certainly could have put the store instance into createContext, but there's several reasons why it made sense to put the store state into context instead.

The largest reason is to improve compatibility with "concurrent React", because the entire tree will see a single consistent state value. The very short explanation on this is that React's "time-slicing" and "Suspense" features can potentially cause problems when used with external synchronous state management tools. As one example, Andrew Clark has described "tearing" as a possible problem, where different parts of the component tree see different values during the same component tree re-render pass. By passing down the current state via context, we can ensure that the entire tree sees the same state value, because React takes care of that for us.

(This then turned out to be too early as there's not yet a way to bail out with useContext... ref: reduxjs/react-redux#1177)


On the other hand, this project is fine with context as long as we can use observedBits. ref #5

from react-hooks-global-state.

jacob-ebey avatar jacob-ebey commented on July 29, 2024

Thanks for the link. Does "tearing" here refer to the visual representation of the vdom being out of sync current tree?

from react-hooks-global-state.

dai-shi avatar dai-shi commented on July 29, 2024

I too am not sure, but probably it's about different parts in the single vdom tree.

You can learn some more about it here. I think I follow the technique described there for my other library that uses subscriptions. I can't say for sure as the description is not very specific to hooks (at least for me).

from react-hooks-global-state.

dai-shi avatar dai-shi commented on July 29, 2024

Closing this issue. Feel free to open a new one.

from react-hooks-global-state.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.