create-hooks-slice is a simple tool to generate redux hooks, focus on the association.
How to generate reducer and action? full example
- put a pure function object named
reducers
,initialData
andname
in the options
- name is the reducer name, should be unique for redux store.
- initialData provide initial data and data structure
- reducers a function object, the function accept two arguments,
data
is the unique data you will change,payload
is the data passed from action. you can use nornal way to do the data mutation, the tool keepsdata immutable
, it's benefit with library immerjs.
import createHooksSlice from "create-hooks-slice";
const initialData: { list?: Pet[], selectedId?: number, current?: Pet } = {};
const petSlice = createHooksSlice({
name: "pet",
initialData,
reducers: {
availableList(data, payload: Pet[]) {
data.list = payload;
},
selectedPet(data, payload: Pet) {
data.current = payload;
},
},
});
- export
actionHooks
,name
,reducer
from the result
const { name: petReducerName, reducer: petReducer, actionHooks } = petSlice;
const store = createStore(
combineReducers({
[petReducerName]: petReducer,
})
);
- export various hooks with prefix
useAction
- the hook name is generated by function name in the
reducers
passed in the opitons.
const { useActionAvailableList, useActionSelectedPet } = actionHooks;
- use exported action hooks in the function component
- call action hooks at the top level, similar with
useDispatch
, and get the sepecific dispatch functions, call them in proper place. - the only accepted argument for the dispatch function has the same structure with the
payload
defined in the correspondingreducer function
export function TestAction() {
const pets: Pet[] = [
{ id: 1, name: "kitty", status: "available" },
{ id: 2, name: "mimmk", status: "available" },
];
const { useActionAvailableList, useActionSelectedPet } = actionHooks;
// get action function after calling action hook
const dispatchAvailableList = useActionAvailableList();
const dispatchSelectedPet = useActionSelectedPet();
useEffect(() => {
// it's similar with `dispatch({ type: "pet/availableList", payload: pets });`
dispatchAvailableList(pets);
dispatchSelectedPet(pets[0]);
}, []);
return null;
}
How to handle side effect, data fetching, thunk? full example
- put a pure function object named
thunks
in the options
- the function in the
thunks
are pure fetch function, accept one optionalquery
agrument, and return a promise. - if the
thunk name
is same withreducer name
in thereducers
, will automatic dispatch the coresponding action, store the fetch data to the redux store.
const petSlice = createHooksSlice({
... ...
thunks: {
availableList() {
console.log("fetching pets...");
return fetch(`${API_HOST}pet/findByStatus?status=available`).then(
(resp) => resp.json()
);
},
selectedPet(id: number) {
return fetch(`${API_HOST}pet/${id}`).then((resp) => resp.json());
},
},
});
- export
thunkHooks
from the result and various hooks with prefixuseThunk
-
- the hook name is generated by function name in the
thunks
passed in the opitons.
- the hook name is generated by function name in the
const {
name: petReducerName,
reducer: petReducer,
actionHooks,
thunkHooks,
} = petSlice;
const { useThunkAvailableList, useThunkSelectedPet } = thunkHooks;
- use exported thunk hooks in the function component
- call thunk hooks at the top level, similar with
useDispatch
, and get the sepecific dispatch functions, call them in proper place. - there are two accepted arguments for the dispatch function, fist one has the same structure with the
query
defined in the correspondingthunk function
- the scecond one is a callback,
(result: Result) => void | (result: Result) => { pending?: (result: Result) => void, fulfilled: (result: Result) => void, rejected?: (error: Error) => void }
- the dispatch function return the Promise of result, so can use
await
andtry-cacth
export function TestThunk({ children }: { children?: React.ReactNode }) {
const { useThunkAvailableList, useThunkSelectedPet } = thunkHooks;
// get thunk function after calling action hook
const dispatchThunkAvailableList = useThunkAvailableList();
const dispatchThunkSelectedPet = useThunkSelectedPet();
return (
<div
onClick={async () => {
// it's similar with `dispatch((dispatchFun) => { fetch("pet/findByStatus?status=available") ... });`
const result = await dispatchThunkAvailableList();
const pets: Pet[] = result;
await dispatchThunkSelectedPet(pets[0].id);
}}
>
{children}
</div>
);
}
How to select data from redux store, how to cache computing value? full example
- put a pure function object named
selectors
in the options
- the function in the
selectors
are pure fetch function, accept onedata
agrument, it's the unique data from the redux store. - the result of the function is the data you want to transform from the original redux data, compute the data with normal way, the tool will help you decide whether reading from cache or re-computing. it's benefit with library proxy-memoize.
const petSlice = createHooksSlice({
... ...
selectors: {
pets(data) {
console.log("select pets...");
return data.list
? data.list.map((pet) => {
const result = {
id: pet.id,
name: pet.name,
photoUrls: pet.photoUrls || [],
tags: pet.tags ? pet.tags.map((tag) => tag.name) : []
};
return result;
})
: [];
},
tags(data) {
console.log("select tags...");
const tagSet = new Set<string>();
data.list?.forEach((pet) => {
pet.tags?.forEach((tag) => {
if (tag) tagSet.add(tag.name);
});
});
return Array.from(tagSet);
},
currentPet(data) {
console.log("select current pet...");
return data.current;
}
}
});
- export
selectorHooks
from the result and various hooks with prefixuseSelector
-
- the hook name is generated by function name in the
selectors
passed in the opitons.
- the hook name is generated by function name in the
const {
name: petReducerName,
reducer: petReducer,
actionHooks,
thunkHooks,
selectorHooks,
} = petSlice;
const { useThunkAvailableList, useThunkSelectedPet } = thunkHooks;
- use exported thunk hooks in the function component
- call selector hooks at the top level, similar with
useSelector
, but don't need pass a function, you pre-define the transformer in theselectors
, the hook result is the transformed data you needed. - if you want to compute and transform data with component props, the selector hooks accept two agruments, a transformer function
(selectorData: SelectorData) => any
, and a dependecies, it's very similaruseMemo
. - the component which use the selector hook will trigger re-render by the
immutable
of the hook result.
export function TestSelector() {
const { useSelectorPets, useSelectorTags } = selectorHooks;
// select and cache pets from redux store
const pets = useSelectorPets();
const tags = useSelectorTags();
// use `useMemo` to cache computed values, it's similar with `useMemo(transformerFunc, [data, ...deps])`
const petNames = useSelectorPets((pets) => {
console.log("compute petNames...");
return pets.map((pet) => pet.name);
}, []);
return (
<>
<div>{`pets: ${petNames.join(", ")}`}</div>
<div>{`tags: ${tags.join(", ")}`}</div>
</>
);
}
How to make a strategy to cache or invalidate remote data? full example
- put a pure function object named
resources
in the options
resource
only fetch once when dependencies don't be changed, similar but differnet withuseEffect
, it was globally cached in redux store. Inspired byrtk-query
andswr
resources
is a key-value map, it connectsselector
andthunk
, the key is theselector name
, and value is thethunk name
.
const petSlice = createHooksSlice({
... ...
resources: {
pets: "availableList",
tags: "availableList",
},
});
- export
resourceHooks
from the result and various hooks with prefixuseResource
,useSuspense
- the hook name is generated by function name in the
rsources
passed in the opitons. - actually
useSuspense
is same withuseResource
, just be applied forReact.Suspense
scenario.
const {
name: petReducerName,
reducer: petReducer,
actionHooks,
thunkHooks,
selectorHooks,
resourceHooks,
} = petSlice;
const { useResourceTags, useResourcePets } = thunkHooks;
// these for React.Suspense
const { useSuspenseTags, useSuspensePets } = thunkHooks;
- use exported thunk hooks in the function component
- call selector hooks at the top level, the hook acccept two arguments, the first one is the
query
fromthunk function
, the second one is thedependencies
, decide whether re-fetch again. - if you don't pass second agrument, the data fetching only happen once, it's a global cache, different
useResource
with samethunk function
, only the first called hook trigger fetching, the others only select the data. - the result of hook
- data: the data from selector
- useData: a specific hook for the data, it's a alternative of the relevant
useSelector
, if you want to fully usinguseResource
instead ofuseSelector
- isLoading: first data fetching status.
- isLoaded: first data loaded successfully.
- isFetching: every data fetching status.
- isSuccess: every data loaded successfully.
- isError: error status when fetching failed.
- error: error data.
- refetch: force re-fetch again regardless of status and dependencies.
export function PetNav() {
const { useResourceTags } = resourceHooks;
const {
data: tags,
useData,
refetch,
isLoaded,
isLoading,
isSuccess
} = useResourceTags();
return (
<section>
<h1>tags: </h1>
{!isLoaded && isLoading ? (
<div>{"first loading..."}</div>
) : (
isSuccess && (
<div style={{ display: "flex", cursor: "pointer" }}>
<div style={{ marginRight: 20 }} onClick={refetch}>
{"ALL"}
</div>
{tags.map((tag, index) => (
<div
key={index}
style={{ marginRight: 20, cursor: "pointer" }}
onClick={refetch}
>
{tag && tag.toUpperCase()}
</div>
))}
</div>
)
)}
</section>
);
}
export function PetList() {
const { useResourcePets } = resourceHooks;
const {
data,
useData,
isLoading,
isLoaded,
isFetching,
isSuccess
} = useResourcePets();
// use `useMemo` to cache computed values, it's similar with `useMemo(transformerFunc, [data, ...deps])`
const normalizedPets = useData((pets) => {
return pets.map((pet) => {
return {
id: pet.id,
name: pet.name,
photoUrls: pet.photoUrls.join(", "),
tags: pet.tags.join(", ")
};
});
}, []);
return (
<section>
<h1>pets: </h1>
{!isLoaded && isLoading ? (
<div>{"first loading..."}</div>
) : isFetching ? (
<div>{"fetching..."}</div>
) : (
isSuccess && (
<div>
... ...
</div>
)
)}
</section>
);
}