DEV Community

tkow
tkow

Posted on

Declarative and inferable types way in React Redux

Motivation

Recently I decided to replace partial redux asynchronous logics using thunk actions to useSWR.

Nowadays, we drives into frequently switching state management libraries as modern frontend development grows up. The speed's surprising and it's still going to be more sophisticated.

As it is, writing code declarative as long as possible is significant important to reduce costs to switch libraries.

This article describes how your code gets declarative when you use react-redux.

Where your code should be declarative

We often use @reduxjs/toolkit to manage redux, but you can use vanilla redux and react-redux, too. What's important in this situation is redux dispatchers behaves as if simple javascript functions because the former are redundant form compared to the latter. The dispatch looks enough declarative thanks to action type as long as dispatcher pattern stay main stream, but someday paradigm shift may come that you don't use any libraries have dispatcher pattern.
So, you should write dispatch as less as possible.

As my opinion, useSelector is enough declarative when your selector input can be replaceable by switching the selector function. The definition's desirable to be out of scope from React component and input it to useSelector as function reference.

Utility

As first, we ready utility type to map redux actions to normal javascript functions like followed by code. Add your redux state type for RootState Type to the code.

import { useSelector as _useSelector, useDispatch as _useDispatch } from 'react-redux'

import { ThunkDispatch, Action, bindActionCreators, ThunkAction, ActionCreatorsMapObject } from '@reduxjs/toolkit'

export const useSelector = <M>(state: (state: RootState) => M, equalityFn?: ((left: M, right: M) => boolean)): M => _useSelector<RootState, M>(
  state, equalityFn
)

export const useDispatch = <IAction extends Action>(): ThunkDispatch<RootState, any, IAction> => {
  return _useDispatch<ThunkDispatch<RootState, any, IAction>>()
}

type ThunkActionCreator<ReturnType, State, ExtraThunkArg, BasicAction extends Action> = (...args: any[]) => ThunkAction<ReturnType, State, ExtraThunkArg, BasicAction>

type BoundActionCreators<ActionCreators> = {
  [key in keyof ActionCreators]:
  ActionCreators[key] extends ThunkActionCreator<infer R, RootState, any, Action> ?
  (...args: Parameters<ActionCreators[key]>) => R :
  ActionCreators[key]
}

export const useBindActionCreators = <
  A extends Action,
  ActionCreators extends ActionCreatorsMapObject<A | ThunkActionCreator<any, RootState, any, Action>>
>(actionCreators: ActionCreators): BoundActionCreators<ActionCreators> => {
  const dispatch = _useDispatch<ThunkDispatch<RootState, any, A>>()
  return useMemo(() => {
    return bindActionCreators(actionCreators, dispatch)
  }, [actionCreators, dispatch]) as BoundActionCreators<ActionCreators>
}
Enter fullscreen mode Exit fullscreen mode

These utility functions maps redux states and dispatchers to react requires props by useSelectors and useBindActionCreators. I recommend you should use bindActionCreators in React components because dispatch's desired as less as possible as it have been already described.

Examples

As first, see the examples you may notice it has room for improvement. Given that, the file of path ex/react-redux includes above utility functions to infer types and the file of path user/actions includes thunk action's named fetchUser which fetches login user data and simple action's named setUser. Fetching data has id and name, and user state type is same.

import { useSelector, useDispatch, useState } from 'ex/react-redux'
import * as userActions from 'user/actions'

function NameInputExample () {
  const userName = useSelector((state) => state.userName)
  const dispatch = useDispatch()

  const onChange = (input) => {
    dispatch(userActions.setUserName(input.target.value))
  }

  const [initialized, setInitialized] = useState()

  useEffect(()=> {
    fetchUser().then(() => setInitialized(true) )
  }, [fetchUser])

  if(!initialized) return null

  return <input type="text" name="user_name" value={userName} onChange={onChange} />
}
Enter fullscreen mode Exit fullscreen mode

It's reasonably simple, but you may notice it's not handy to replace onChange handler if you wants to switch logics to use libraries use no dispatcher pattern. The sample has only one function but, if you have many handlers which you use dispatch and
write directly the definition in the component, you must remove them all.

To avoid it, take the example rewritten.

import { useEffect } from 'react'
import { useSelector, useBindActionCreators } from 'ex/react-redux'
import * as userActions from 'user/actions'

const userNameSelector = (state) => state.userName

function NameInputExample () {
  const userName = useSelector(userNameSelector)
  const { setUserName, fetchUser } = useBindActionCreators(userActions)

  const onChange = (input) => {
    setUserName(input.target.value)
  }

  const [initialized, setInitialized] = useState()

  useEffect(()=> {
    fetchUser().then(() => setInitialized(true) )
  }, [fetchUser])

  if(!initialized) return null

  return <input type="text" name="user_name" value={userName} onChange={onChange} />
}
Enter fullscreen mode Exit fullscreen mode

This code becomes declarative now about the selector and the handler logic. All you need only to replace return values

of useBindActionCreators to change setUserName logic and you don't need change login in component. This example is great enough, but it'll be more declarative because useSelector and useBindActionCreators still has derived from redux.

Advance Examples

From this section, I introduce some tips how to write declarative code. So, you can pick them if you like my ideas.

Similar react useState form

If your need state and the modifier are always pairs, its' convenient you imitate your custom hook on useState.

import { useSelector, useBindActionCreators } from 'ex/react-redux'
import * as user from 'user/actions'

const userNameSelector = (state) => state.userName

const useUserNameState = () => {
  const userName = useSelector(userNameSelector)
  const { setUserName } = useBindActionCreators(user)
  return [userName, setUserName]
}

const useInitialization = () => {
  const { fetchUser } = useBindActionCreators(user)
  const [initialized, setInitialized] = useState()
  useEffect(()=> {
    fetchUser().then(() => setInitialized(true) )
  }, [fetchUser])
  return initialized
}

function NameInputExample () {
  const [userName, setUserName] = useUserNameState()

  const onChange = (input) => {
    setUserName(input.target.value)
  }

  const initialized = useInitialization()

  if(!initialized) return null

  return <input type="text" name="user_name" value={userName} onChange={onChange} />
}
Enter fullscreen mode Exit fullscreen mode

This advantage is easy to replace it useUserNameState to useState.

Similar MVVM form

I recommend to map values to your props via object like instance value. It may looks connect utility function in redux. Furthermore, if you are familiar to object-oriented programming, you notice the values in custom hooks are similar to private variables and exported some of them are getters, and the export methods are public methods.
If you understand this analogy, you can write clean code.

import { useSelector, useBindActionCreators } from 'ex/react-redux'
import * as user from 'user/actions'

const userNameSelector = (state) => state.userName

const useMapProps = () => {
  const userName = useSelector(userNameSelector)
  const {setUserName} = useBindActionCreators(user)
  return {
    userName, 
    setUserName
  }
}

function NameInputExample (props) {
  const {userName, setUserName, fetchUser } = useMapProps(props)

  const onChange = (input) => {
    setUserName(input.target.value)
  }

  const [initialized, setInitialized] = useState()

  useEffect(()=> {
    fetchUser().then(() => setInitialized(true) )
  }, [fetchUser])

  if(!initialized) return null

  return <input type="text" name="user_name" value={userName} onChange={onChange} />
}
Enter fullscreen mode Exit fullscreen mode

This advantage is to capsulize not used values and statements in react component and easy to replace it to instance (including singleton) or any other. Its' very flexible.

About other model

Though it's of course possible you arbitrary how you map your values to your components. I don't recommend nest object mapping or separated state and handler scope. The former make hard to improve rendering performance because you need to compare references for that and they often become more complicated than immutable values.The reason of the latter is you often see that the handers depends on the state's values. Sometimes you are inevitable to mix side effects in your custom hooks or mess up codes for some reasons like deadline comes near. When you're in the situation, the patterns in this article can't be applied entirely in your projects. What is important is to understand how wide you will need to replace codes and how certain to the extent you need declarative codes.

Conclusion

  • You can use fully type inferences for useSelector and useDispatch with bindActionCreators introduced by this article.
  • I recommend to map redux logics to simple js functions as long as possible before starting to use them in a React Component if you use redux.

Latest comments (0)