renatorib / react-powerplug

:electric_plug: [Unmaintained] Renderless Containers

Home Page:https://renatorib.github.io/react-powerplug

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

add `reset/resetState`?

ianstormtaylor opened this issue · comments

Hey, thanks for this great library!

I find myself setting initial= values, updating them over time, and then performing an action at a certain point. And then, often after performing the action I need to "reset" the values to their initial state again, to build towards another action.

It would be nice if in addition set(value), the components were passed a reset() function as well that always reset the value to the initial value.


(Side note, why is <State> the only component that takes the function named setState instead of simply set to stay consistent with the others?)

Thanks @ianstormtaylor 😄

I like that reset api. To State Containers I think it makes sense.

The reason for the setState to be different from the others, is that users are already comfortable with state / setState of a common component. In fact, State's setState works exactly like the setState of a Stateful Component.

Before (0.1x), the set functions were more descriptive <Value>{({ setValue })}</Value>, <List>{({ setList })}</List>. But some were not so clear as that <Toggle>{({ setOn })</Toggle>

I thought that if I kept the same prop for all (set()) I would guarantee a greater consistency. But I've kept it from setState for the reasons set out above.


PS: I hate the fact that Value has a value state, List has a list state, but Toggle has on state. I think it breaks consistency. But I don't know how to solve this problem Toggle is much more semantic than <On> (??). <Map> and <Set> also have different behaviors.

Here's how I usually use things

<Toggle>
  {drawer =>
    <>
      <button onClick={drawer.toggle}>{drawer.on ? 'close' : 'open'}</button>
      {drawer.on && <Drawer onClose={() => drawer.set(false)} />}
    </>
  }
</Toggle>

Unified fields work good with specific containers. State container in opposite is generic and can't have specific name. I use it always like this

<State initial={{ on: false }}>
  {({ state, setState }) =>
    <>
      <button onClick={() => setState({ on: true })}>open</button>
      {state.on && <Drawer onClose={() => setState({ on: false })} />}
    </>
  }
</State>

Thanks for your point @TrySound!
What you think about reset()?

I'm not sure. It can be implemented easy by hoisting initial value. However this value can be changed with a time and I'm not sure what is the right thing to set the new one or the first one. It depends on user requirements. I didn't have such use case tbh. It looks too opinionated.

How is to "too opinionated"? It's a nice convenience that the library can make extremely easy with minimal logic, or everyone can reinvent themselves with the logic spread out over many places. I think anything that exposes initial and set should also expose reset.

Otherwise resetting the state will always involve either duplicating the initial value in both places... or being forced to store the initial state as a variable, which means you have to change things from simple => (<...>) to => { const initial = ...; return (<...>); } which is a big hassle.

@renatorib that makes sense about the setState vs. set distinction. I think it might be worth adding a set -> setState alias for consistency personally, but that's your call.

The way I'd thinking about it is that it's good that everything is always set for consistency. But then that the <State> component happens to offer a setState alias, for people who are used to that, and for making the transition easier, but treat the setState as the edge case, and set the standard.

Edit: For what it's worth, the on in Toggle didn't bother me. Although I do appreciate that level of detail 😄 If you want to get super pedantic, the <Counter>{({ count })}</Counter> is a bit uneven, and should really be <Count>{({ count })}</Count> instead to match.

Aliases are a bad for api design. It means we can't decide how feature should be used. There should be only one way to do things.

I agree with @TrySound about aliases things. I'm resilient about creating aliases.
I also agree with @ianstormtaylor with reset as a good convenience. Powerplug is primarly about convenience.

I'm not going to make a decision now, but I'm really thinking of implementing the reset() in the near future, to all components that have initial. As it is not a breaking change, I believe it might be in a minor of 1.x. I really do not want to create any more changes for the 1.0 release.

Anyway, I'll leave this issue open until I decide what to do!

FWIW, this tweet kind of sums up the reasoning behind my alias argument: https://twitter.com/sebmarkbage/status/1005162462486900737

I still think that every component should expose set as the "canonical" prop name. And then specifically the <State> component should expose that same function as setState with an alias.

By calling it set (and reset) on every component, you get consistency. And consistency makes things easier to use, but also allows for more dynamic programming type use cases (eg. iterating over multiple Powerplug components at once).

But at the same time, while programming, people familiar with React are going to naturally mis-remember the { state, set } props as { state, setState }. So providing the setState alias makes that potential pitfall go away, making the API slightly easier to use still.


Of course, I also agree that many different ways to do things is not good. But in this case, I think it's really just exposing a single "thing" in both a canonical way, for consistency, and in a "guessable" way, to avoid frustration.

Anyways, food for thought.

IMO reset is needed, imagine following (of course bad ;-)) code.

({someProp}) => 
  <Value initial={someProp}>{
     val => <Button onClick={() => someAsync(() => {
        // at the moment this callback is called, someProp is already has changed, 
       // and I need to reset Value to it's initial, but calling 
      // val.set(someProp) isn't same as reset, as callback contains previous version of someProp
     // so val.reset() is needed
     }) }

here is simple reproducible example of a problem, even without async

https://codesandbox.io/s/54nyo0r78p

If I click inc, then send to server the console output is:

initial set to 1
val is 1
val is 2 // I clicked inc
send complete // I clicked send 2 server
initial set to 2 // see initial has changed
val is 1 // see I can't reset!

To solve this now, I need to wrap App in an instance so this.props.data will contain the latest props, so I could reset. BTW having an instance I don't need to use Value as I already have state ;-)