Utilities to help construct "normalized" states when using useReducer
in react-hook
.
Adopting concept of redux-duck
src/thunk-reducer.ts is adopted from nathanbuchar/react-hook-thunk-reducer.
react-reducer-utils
is with the following additional features:
- The development of the reducers follows the concept of
redux-duck
. - Similar to
mapDispatchToProps
, the bound-dispatched-actions are generated throughuseReducer
.
Please check docs/00-introduction.md for more information.
- Starting from
5.0.1
, this library supports only typescript. - Starting from
5.2.2
, There is no side effect whether or not putting reducer functions to the dependency ofuseEffect
. - Starting from
6.0.0
:- reducer.default is optional.
- more types for type-hint for editors.
- requiring explicit type definition in useReducer.
- Starting from
7.1.0
: requiringStateType.LOCAL
for useReducer.
npm install react-reducer-utils
Reducer able to do increment (reducers/increment.ts):
import { init as _init, setData, createReducer, Thunk, getState, type State as rState, genUUID } from 'react-reducer-utils'
export const myClass = 'demo/Increment'
export interface State extends rState {
count: number
}
export const defaultState: State = {
count: 0
}
export const init = (): Thunk<State> => {
const myID = genUUID()
return async (dispatch, getClassState) => {
dispatch(_init({myID, state: defaultState}))
}
}
export const increment = (myID: string): Thunk<State> => {
return async (dispatch, getClassState) => {
let classState = getClassState()
let me = getState(classState, myID)
if(!me) {
return
}
dispatch(setData(myID, { count: me.count + 1 }))
}
}
App.tsx:
import { type ModuleToFunc, useReducer, getRootID, getState } from 'react-reducer-utils'
import * as DoIncrement from './reducers/increment'
type TDoIncrement = ModuleToFunc(typeof DoIncrement)
type Props = {
}
export default (props: Props) => {
const [stateIncrement, doIncrement] = useReducer<DoIncrement.State, TDoIncrement>(DoIncrement, StateType.LOCAL)
//init
useEffect(() => {
doIncrement.init()
}, [])
// to render
const incrementID = getRootID(stateIncrement)
const increment = getState(stateIncrement)
if(!increment) {
return (<div styles={{display: 'none'}}></div>)
}
return (
<div>
<p>count: {increment.count}</p>
<button onClick={() => doIncrement.increment(incrementID)}>increase</button>
</div>
)
}
import type { State as rState } from 'react-reducer-utils'
// reducer class name.
export const myClass = ""
// state definition of the reducer.
export interface State extends rState {
}
.
.
.
import { type ModuleToFunc, useReducer, getRootID, getState } from 'react-reducer-utils'
import * as DoModule from '../reducers/module'
type TDoModule = ModuleToFunc<typeof DoModule>
const Component = () => {
const [stateModule, doModule] = useReducer<DoModule.State, TDoModule>(DoModule, StateType.LOCAL)
const moduleID = getRootID(stateModule)
const theModule = getState(stateModule)
.
.
.
}
The general concept of normalized state can be found in Normalizing State Shape with the following features:
- ClassState: the state of the class, including the nodes and the root of the class.
- NodeState: the state of a node, including the id, children, parent, links of the node, and the content (state) of the node.
- State: the content of the node, represented as a state.
- The concept of "parent" and "children" and "links" is embedded in the NodeState.
- remove (me):
- initiate "remove" for all the children.
- remove from the parent.
- remove from all the links.
- remove child:
- the child initiate "remove".
- remove link:
- the link initiate "remove link" on me.
- remove (me):
- To avoid complication, currently there is only 1 parent.
For example, the example in the redux link is represented as:
statePost = {
myClass: 'post',
doMe: (DispatchedAction<Post>),
nodes: {
[uuid-post1] : {
id: uuid-post1,
state: {
author : uuid-user1,
body : "......",
},
_parent: {
id: uuid-user1,
do: doUser
},
_links: {
comment : {
list: [uuid-comment1, uuid-comment2],
do: doComment
}
}
},
[uuid-post2] : {
id : uuid-post2,
state: {
author : uuid-user2,
body : "......",
},
_parent: {
id: uuid-user2,
do: doUser
},
_links: {
comment : {
list: [uuid-comment3, uuid-comment4, uuid-comment5],
do: doComment
}
}
}
}
}
and:
stateComment = {
myClass: 'comment',
doMe: (DispatchedAction<Comment>),
nodes: {
[uuid-comment1] : {
id: uuid-comment1,
state: {
author : uuid-user2,
comment : ".....",
},
_parent: {
id: uuid-user2,
do: doUser
},
_links: {
post: {
list: [uuid-post1],
do: doPost
}
}
},
[uuid-comment2] : {
id : uuid-comment2,
state: {
author : uuid-user3,
comment : ".....",
},
_parent: {
id: uuid-user3,
do: doUser
},
_links: {
post: {
list: [uuid-post1],
do: doPost
}
}
},
[uuid-comment3] : {
id : uuid-comment3,
state: {
author : uuid-user3,
comment : ".....",
},
_parent: {
id: uuid-user3,
do: doUser
},
_links: {
post: {
list: [uuid-post2],
do: doPost
}
}
},
[uuid-comment4] : {
id : uuid-comment4,
state: {
author : uuid-user1,
comment : ".....",
},
_parent: {
id: uuid-user1,
do: doUser
},
_links: {
post: {
list: [uuid-post2],
do: doPost
}
}
},
[uuid-comment5] : {
id : uuid-comment5,
state: {
author : uuid-user3,
comment : ".....",
},
_parent: {
id: uuid-user3,
do: doUser
},
_links: {
post: {
list: [uuid-post2],
do: doPost
}
}
},
}
}
and:
stateUser = {
myClass: 'user',
doMe: (DispatchedAction<User>),
nodes: {
[uuid-user1] : {
id: uuid-user1,
state: {
username : "user1",
name : "User 1",
},
_children: {
post: {
list: [uuid-post1],
do: doPost,
},
comment: {
list: [uuid-comment4],
do: doComment,
}
}
},
[uuid-user2] : {
id: uuid-user2,
state: {
username : "user2",
name : "User 2",
},
_children: {
post: {
list: [uuid-post2],
do: doPost,
},
comment: {
list: [uuid-comment1],
do: doComment,
}
}
},
[uuid-user3] : {
id: uuid-user3,
state: {
username : "user3",
name : "User 3",
},
_children: {
post: {
list: [uuid-post1],
do: doPost,
},
comment: {
list: [uuid-comment2, uuid-comment3, uuid-comment5],
do: doComment,
}
}
}
}
}
Similar to React.useReducer
, but we use useThunkReducer
, and we also bind the actions with dispatch (similar concept as mapDispatchToProps
).
return: [ClassState<S>, DispatchedAction<S>]
initializing the react-object.
generate uuid for react-object.
set the data to myID.
remove the react-object.
params:
- reduceMap:
{}
representing the mapping of the additional reduce-map. referring to DEFAULT_REDUCE_MAP.
get the object in the state.
get the root id.
get the root-object in the state.
get the root node.
get the object in the state.
get the child-ids from the childClass in me.
get the only child-id (childIDs[0]) from the childClass in me.
get the link-ids from the linkClass in me.
get the only link-id (linkIDs[0]) from the linkClass in me.
params:
- child:
{id, theClass, do}
remove the child (and delete the child) from myID.
params:
- link:
{id, theClass, do}
remove the link from myID (and remove the link from linkID).