piotrwitek / typesafe-actions

Typesafe utilities for "action-creators" in Redux / Flux Architecture
https://codesandbox.io/s/github/piotrwitek/typesafe-actions/tree/master/codesandbox
MIT License
2.41k stars 99 forks source link

Allow createReducer to be used without initial state #259

Open lnhrdt opened 2 years ago

lnhrdt commented 2 years ago

We are using createReducer throughout a React application. We always use these reducers with React's useReducer hook. In this scenario it is cumbersome to supply the initial state to both createReducer and to useReducer. It's especially cumbersome when the initial state is initialized by the runtime context – I can build the state at runtime and pass it to useReducer, but then the initial state passed to createReducer is not used. Understanding which initial states are real and which ones are just placeholders that will never be used has been a source of confusion for our team.

Ideally we could just rely on the type parameter passed to createReducer to build a reducer rather than also having to supply an initial state that may never be used.

piotrwitek commented 2 years ago

Hey @lnhrdt, thanks for sharing your feedback. Could you please provide the usage examples for your use case so I could fully understand what is the source of confusion. Thanks

Also please understand this library wasn't designed to be used in such a way but let me see so I can explain what is the root issue here and perhaps work out a new developer friendly utility that would be designed for such usage.

lnhrdt commented 2 years ago

Hello @piotrwitek! Thanks for getting back to me and apologies for the delay. Here's an annotated example.

import React, {Reducer, useReducer} from 'react'
import {ActionType, createAction, createReducer, getType} from 'typesafe-actions'

type State = {
  height: number
}

const actions = {
  addHeight: createAction('ADD_HEIGHT')<number>(),
  doubleHeight: createAction('DOUBLE_HEIGHT')(),
}

type Action = ActionType<typeof actions>

/*
 * We don't need to declare an initial state with this reducer.
 * The useReducer hook requires it anyway down below, so this is good.
 * */
const switchReducer: Reducer<State, Action> = (prevState, action) => {
  switch (action.type) {
    case getType(actions.addHeight): {
      return {
        height: prevState.height + action.payload
      }
    }
    case getType(actions.doubleHeight): {
      return {
        height: prevState.height * 2
      }
    }
  }
}

const AppWithSwitchReducer = () => {
  /*
  * Using React's useReducer here, we are required to provide the initial state.
  * */
  const [state, dispatch] = useReducer(switchReducer, {height: 0})
  return (
    <div>
      <div>height: {state.height}</div>
      <button onClick={() => dispatch(actions.addHeight(5))}>add 5</button>
      <button onClick={() => dispatch(actions.doubleHeight())}>double</button>
    </div>
  )
}

/*
 * This builder patter is super cool, compact, and readable!
 * But there is no way to use it without providing an initial state.
 * This is unfortunate because while in this example the state is simple,
 * in realistic cases the state is very complex and having initial state in 2 places is confusing.
 * I wish we could use the builder pattern without providing an initial state here.
 * */
const builderReducer: Reducer<State, Action> = createReducer<State, Action>({height: 0})
  .handleAction(actions.addHeight, (state, action) => ({height: state.height + action.payload}))
  .handleAction(actions.doubleHeight, (state) => ({height: state.height * 2}))

const AppWithBuilderReducer = () => {
  /*
  * Again, React's useReducer requires us to provide the initial state.
  * */
  const [state, dispatch] = useReducer(builderReducer, {height: 0})
  return (
    <div>
      <div>height: {state.height}</div>
      <button onClick={() => dispatch(actions.addHeight(5))}>add 5</button>
      <button onClick={() => dispatch(actions.doubleHeight())}>double</button>
    </div>
  )
}

Does that help clarify the way I'm trying to use your library?

lnhrdt commented 2 years ago

Hello @piotrwitek were you able to read through my example or do you have any questions?

piotrwitek commented 2 years ago

Hey @lnhrdt thanks a lot for clarification, it's clear! I just missed your notification. I will have a look this week, but I agree with you that this is a generic pattern that could be a nice fit into the library. Let me check the details so I can prepare a proposal for such a feature.

lnhrdt commented 6 months ago

Believe it or not, we're still using the library and still wishing for this feature.

Any chance of continued development in this project @piotrwitek, or have you moved on?

lnhrdt commented 5 months ago

Hey @piotrwitek! We ended up moving to a new solution in our application. I thought I'd mention it here in case you ever come back to this issue–we've already moved on and wouldn't benefit from the feature anymore.

Thank you for this library, regardless. We used it happily for a long time.