DEV Community

terrierscript
terrierscript

Posted on

6 1

Emulate Redux with React hooks

Notice: React Hooks is RFC. This article is experimental

1. use combineReducers with useReducer

We can create nested reducer in redux combineReducer and I try to combine nested reducer and useReducer hook.

import { combineReducers } from "redux"

const counter = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1
    case "DECREMENT":
      return state - 1
  }
  return state
}

const inputValue = (state = "foo", action) => {
  switch (action.type) {
    case "UPDATE_VALUE":
      return action.value
  }
  return state
}

export const rootReducer = combineReducers({
  counter,
  // nest
  someNested: combineReducers({
    inputValue
  })
})

And create components

import React, { useReducer } from "react"

const App = () => {
  const [state, dispatch] = useReducer(rootReducer, undefined, {
    type: "DUMMY_INIT"
  })

  return (
    <div className="App">
      <div>
        <h1>counter</h1>
        <div>count: {state.counter}</div>
        <button onClick={(e) => dispatch({ type: "INCREMENT" })}>+</button>
        <button onClick={(e) => dispatch({ type: "DECREMENT" })}>-</button>
      </div>
      <div>
        <h1>Input value</h1>
        <div>value: {state.someNested.inputValue}</div>
        <input
          value={state.someNested.inputValue}
          onChange={(e) =>
            dispatch({
              type: "UPDATE_VALUE",
              value: e.target.value
            })
          }
        />
      </div>
    </div>
  )
}

I can got good result when pass dummy initialState(=undefined) and any dummy action.

const [state, dispatch] = useReducer(rootReducer, undefined, {
  type: "DUMMY_INIT"
})

2: Create Provider with createContext and useContext

We can avoid passing props with context.

https://reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down

const ReducerContext = createContext()

// Wrap Context.Provider
const Provider = ({ children }) => {
  const [state, dispatch] = useReducer(rootReducer, undefined, {
    type: "DUMMY_INIT"
  })
  return (
    <ReducerContext.Provider value={{ state, dispatch }}>
      {children}
    </ReducerContext.Provider>
  )
}

const App = () => {
  return (
    <Provider>
      <div className="App">
        <Counter />
        <InputValue />
      </div>
    </Provider>
  )
}

For consumer, we can use useContext

const Counter = () => {
  const { state, dispatch } = useContext(ReducerContext)
  return (
    <div>
      <h1>counter</h1>
      <div>count: {state.counter}</div>
      <button onClick={(e) => dispatch({ type: "INCREMENT" })}>+</button>
      <button onClick={(e) => dispatch({ type: "DECREMENT" })}>-</button>
    </div>
  )
}
const InputValue = () => {
  const { state, dispatch } = useContext(ReducerContext)
  return (
    <div>
      <h1>Input value</h1>
      <div>value: {state.someNested.inputValue}</div>
      <input
        value={state.someNested.inputValue}
        onChange={(e) =>
          dispatch({
            type: "UPDATE_VALUE",
            value: e.target.value
          })
        }
      />
    </div>
  )
}

If you want use <Consumer>, like this.


const Counter = () => {
  return (
    <ReducerContext.Consumer>
      {({ state, dispatch }) => {
        return (
          <div>
            <h1>counter</h1>
            <div>count: {state.counter}</div>
            <button onClick={(e) => dispatch({ type: "INCREMENT" })}>+</button>
            <button onClick={(e) => dispatch({ type: "DECREMENT" })}>-</button>
          </div>
        )
      }}
    </ReducerContext.Consumer>
  )
}

3. Emulate bindActionCreactors with useCallback

If we want bind action, we can use useCallback

  const increment = useCallback((e) => dispatch({ type: "INCREMENT" }), [
    dispatch
  ])
  const decrement = useCallback((e) => dispatch({ type: "DECREMENT" }), [
    dispatch
  ])
  const updateValue = useCallback(
    (e) =>
      dispatch({
        type: "UPDATE_VALUE",
        value: e.target.value
      }),
    [dispatch]
  )
  return <div>
   :
    <button onClick={increment}>+</button>
    <button onClick={decrement}>-</button>
   :
  </div>

4. Emulate mapStateToProps and reselect with useMemo

const InputValue = () => {
  const { state, dispatch } = useContext(ReducerContext)
  // memolized. revoke if change state.someNested.inputValue 
  const inputValue = useMemo(() => state.someNested.inputValue, [
    state.someNested.inputValue
  ])

  return (
    <div>
      <h1>Input foo</h1>
      <div>foo: {inputValue}</div>
      <input
        value={inputValue}
        onChange={(e) =>
          dispatch({
            type: "UPDATE_VALUE",
            value: e.target.value
          })
        }
      />
    </div>
  )
}

5. Emulate Container


const useCounterContext = () => {
  const { state, dispatch } = useContext(ReducerContext)
  const counter = useMemo(() => state.counter, [state.counter])
  const increment = useCallback(
    (e) => setTimeout(() => dispatch({ type: "INCREMENT" }), 500),
    [dispatch]
  )
  const decrement = useCallback((e) => dispatch({ type: "DECREMENT" }), [
    dispatch
  ])

  return { counter, increment, decrement }
}

const Counter = () => {
  const { counter, increment, decrement } = useCounterContext()
  return (
    <div>
      <h1>counter</h1>
      <div>count: {counter}</div>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

// 
const useInputContainer = () => {
  const { state, dispatch } = useContext(ReducerContext)
  // memolized action dispatcher
  const updateValue = useCallback(
    (e) =>
      dispatch({
        type: "UPDATE_VALUE",
        value: e.target.value
      }),
    [dispatch]
  )
  // memolized value
  const inputValue = useMemo(() => state.someNested.inputValue, [
    state.someNested.inputValue
  ])
  return {
    updateValue, inputValue
  }
}

const InputValue = () => {
  const { updateValue, inputValue } = useInputContainer()
  return (
    <div>
      <h1>Input foo</h1>
      <div>value: {inputValue}</div>
      <input value={inputValue} onChange={updateValue} />
    </div>
  )
}

Example code

https://stackblitz.com/edit/github-hgrund?file=src/App.js

Extra: middleware

Extra-1: Async fetch

We can emulate middleware with useEffect,But this may not recommended and we wait for Suspence


Reducer

const fetchedData = (state = {}, action) => {
  switch (action.type) {
    case "FETCH_DATA":
      return action.value
  }
  return state
}

We create async function return random value.

const fetchData = (dispatch) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ random: Math.random() })
    }, 100)
  })
  // Really:
  // return fetch("./async.json")
  //   .then((res) => res.json())
  //   .then((data) => {
  //     return data
  //   })
}

Container:
We want pass useEffect to empty array([]).
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

const useFetchDataContainer = () => {
  const { state, dispatch } = useContext(ReducerContext)

  // call on mount only
  useEffect(() => {
    fetchData().then((data) => {
      dispatch({
        type: "FETCH_DATA",
        value: data
      })
    })
  }, [])

  const reload = useCallback(() => {
    fetchData().then((data) => {
      dispatch({ type: "FETCH_DATA", value: data })
    })
  })

  const data = useMemo(
    () => {
      return JSON.stringify(state.fetchedData, null, 2)
    },
    [state.fetchedData]
  )
  return { data, reload }
}

const FetchData = () => {
  const { data, reload } = useFetchDataContainer()
  return (
    <div>
      <h1>Fetch Data</h1>
      <pre>
        <code>{data}</code>
      </pre>
      <button onClick={reload}>Reload</button>
    </div>
  )
}

Extra-2: Emulate custom middleware (like applyMiddleware)

If we need reducer middleware, we can wrap reducer dispatch

// my custom middleware
const myMiddleware = (state, dispatch) => {
  return (action) => {
    if (action.type == "OOPS") { // fire action when `OOPS` action.
      dispatch({ type: "SET_COUNT", value: state.counter + 100 })
    }
  }
}

const useEnhancedReducer = (reducer, enhancer) => {
  const [state, baseDispatch] = useReducer(reducer, undefined, {
    type: "DUMMY_INIT"
  })

  const next = useMemo(() => enhancer(state, baseDispatch), [
    state,
    baseDispatch
  ])

  // wrapped dispatch
  const dispatch = useCallback((action) => {
    baseDispatch(action)
    next(action)
  })

  return { state, dispatch }
}

const Provider = ({ children }) => {
  const { state, dispatch } = useEnhancedReducer(rootReducer, myMiddleware)
  const value = { state, dispatch }
  return (
    <ReducerContext.Provider value={value}>{children}</ReducerContext.Provider>
  )
}

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

Top comments (2)

Collapse
 
budarin profile image

for the emulating reselect

const { state, dispatch } = useContext(ReducerContext);
const counter = useMemo(() => state.counter, [state.counter])

how to avoid re rendering every time the state is changed only when the particular slice of the state is changed?

Collapse
 
terrierscript profile image
terrierscript

Oh, I did not know about that behavior.I will try to find out about it.

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →