0.0.16 • Published 2 years ago

create-hooks-slice v0.0.16

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

create-hooks-slice

create-hooks-slice is a simple tool to generate redux hooks, focus on the association.

How to generate reducer and action? full example

  1. put a pure function object named reducers, initialData and name 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 keeps data 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;
    },
  },
});
  1. export actionHooks, name, reducer from the result
const { name: petReducerName, reducer: petReducer, actionHooks } = petSlice;

const store = createStore(
  combineReducers({
    [petReducerName]: petReducer,
  })
);
  1. 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;
  1. 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 corresponding reducer 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

  1. put a pure function object named thunks in the options
  • the function in the thunks are pure fetch function, accept one optional query agrument, and return a promise.
  • if the thunk name is same with reducer name in the reducers, 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());
    },
  },
});
  1. export thunkHooks from the result and various hooks with prefix useThunk
    • the hook name is generated by function name in the thunks passed in the opitons.
const {
  name: petReducerName,
  reducer: petReducer,
  actionHooks,
  thunkHooks,
} = petSlice;

const { useThunkAvailableList, useThunkSelectedPet } = thunkHooks;
  1. 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 corresponding thunk 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 and try-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

  1. put a pure function object named selectors in the options
  • the function in the selectors are pure fetch function, accept one data 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;
    }
  }
});
  1. export selectorHooks from the result and various hooks with prefix useSelector
    • the hook name is generated by function name in the selectors passed in the opitons.
const {
  name: petReducerName,
  reducer: petReducer,
  actionHooks,
  thunkHooks,
  selectorHooks,
} = petSlice;

const { useThunkAvailableList, useThunkSelectedPet } = thunkHooks;
  1. 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 the selectors, 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 similar useMemo.
  • 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

  1. put a pure function object named resources in the options
  • resource only fetch once when dependencies don't be changed, similar but differnet with useEffect, it was globally cached in redux store. Inspired by rtk-query and swr
  • resources is a key-value map, it connects selector and thunk, the key is the selector name, and value is the thunk name.
const petSlice = createHooksSlice({
  ... ...
  resources: {
    pets: "availableList",
    tags: "availableList",
  },
});
  1. export resourceHooks from the result and various hooks with prefix useResource, useSuspense
  • the hook name is generated by function name in the rsources passed in the opitons.
  • actually useSuspense is same with useResource, just be applied for React.Suspense scenario.
const {
  name: petReducerName,
  reducer: petReducer,
  actionHooks,
  thunkHooks,
  selectorHooks,
  resourceHooks,
} = petSlice;

const { useResourceTags, useResourcePets } = thunkHooks;
// these for React.Suspense
const { useSuspenseTags, useSuspensePets } = thunkHooks;
  1. 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 from thunk function, the second one is the dependencies, 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 same thunk 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 using useResource instead of useSelector
    • 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>
  );
}
0.0.16

2 years ago

0.0.15

2 years ago

0.0.14

2 years ago

0.0.13

2 years ago

0.0.12

2 years ago

0.0.11

2 years ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.3

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago