TrypicalDev
TrypicalDev

Reputation: 111

context hook cant store value

Code below works fine except when call Add function which adds new number to array, array contains only last added item.

dont understand why it does not store IDs

App.ts

import MyComponent from "./main";
import { FavoriteContext, usePostsContextValue } from "./store";

function App() {
  const postsContextValue = usePostsContextValue();
  return (
    <FavoriteContext.Provider value={postsContextValue}>
        <MyComponent />
      </Container>
    </FavoriteContext.Provider>
  );
}

export default App;

Component:

import { FavoriteContext } from "../store";
...

function MyComponent() {
  const [data, setData] = useState<certificates[]>([]);
  const [loading, setLoading] = useState<boolean>(false);
  const [pageNumber, setPageNumber] = useState<number>(1);
  const { IDs, Add, Remove } = useContext(FavoriteContext);



  useEffect(() => {
    console.log("IDs", IDs); //logs only 1 item, last selected Id
  }, [IDs]);

  const handleAddClick= async (id: number) => {
    Add(id);
  };


  const columns: TableColumn<certificates>[] = useMemo(() => {
    return [
      {
        name: "STATUS",
        cell: (row) => <strong>{status(row.status)}</strong>,
      },
      {
        cell: (row) => {
          return (
            <IconButton onClick={() => handleAddClick(row.id)}>
              <BookmarkBorderIcon />
            </IconButton>
          );
        },
        button: true,
      },
    ];
  }, []);

  return (
    <DataTable
      columns={columns}
      data={data}
      pagination
      paginationServer
      paginationTotalRows={totalRows}
      onChangeRowsPerPage={handlePerRowsChange}
      onChangePage={handlePageChange}
    />
  );
}

export default MyComponent;

Context.ts:

import React, { useCallback, useMemo, useState } from "react";
import { favouriteType } from "../models";

export const defaultValue: favouriteType = {
  IDs: [],
  Add: (id: number) => {},
  Remove: (id: number) => {},
};
export const FavoriteContext = React.createContext<favouriteType>(defaultValue);

function usePostsContextValue(): favouriteType {
  const [IDs, setIDs] = useState<number[]>([]);

  const Add = (id: number) => {
    setIDs([...IDs, id]);
  };

  const Remove = (id: number) => {
    const newPosts = [...IDs];
    const removedPostIndex = newPosts.findIndex((f) => f === id);
    if (removedPostIndex > -1) {
      newPosts.splice(removedPostIndex, 1);
    }
    setIDs(newPosts);
  };

  return {
    IDs,
    Add,
    Remove,
  };
}

export { usePostsContextValue };

Upvotes: 0

Views: 33

Answers (1)

Hoang
Hoang

Reputation: 166

Every time, a new ID is added, a new Add function is created which would capture the value of the new ID.

  const Add = (id: number) => {
    setIDs([...IDs, id]); // <-- this capture the ID of the outer scope
  };

This works as expected, however looking at the component below:

  const columns: TableColumn<certificates>[] = useMemo(() => {
    return [
      {
        name: "STATUS",
        cell: (row) => <strong>{status(row.status)}</strong>,
      },
      {
        cell: (row) => {
          return (
            <IconButton onClick={() => handleAddClick(row.id)}>
              <BookmarkBorderIcon />
            </IconButton>
          );
        },
        button: true,
      },
    ];
  }, []);

Since this component is never rerendered, the Add function is not updated, so it is always the first version where the captured IDs is empty []. Thus adding one ID is always:

const Add = (id: number) => {
    setIDs([...[], id]); // the first vesion of Add captured IDs = [];
  };

and always have only the last ID.

The solution is to include handleAddClick (which captures the new Add function in the useMemo dep array:

  const columns: TableColumn<certificates>[] = useMemo(() => {
    return [
      {
        name: "STATUS",
        cell: (row) => <strong>{status(row.status)}</strong>,
      },
      {
        cell: (row) => {
          return (
            <IconButton onClick={() => handleAddClick(row.id)}>
              <BookmarkBorderIcon />
            </IconButton>
          );
        },
        button: true,
      },
    ];
  }, [handleAddClick]);

However, this destroys the purpose of useMemo since Add/handleAddClick always recreated on every render, so the solution is to wrap both of them in useCallback.

  const Add = useCallback((id: number) => {
    setIDs([...IDs, id]);
  }, [IDs]); // notice I have to include IDs here so that the function can be recreated with the new IDs.

  const handleAddClick= useCallback(async (id: number) => {
    Add(id);
  }, []);

Better yet, you can use the function syntax of setState, which removes the need to include IDs in the dep array, improving performance.

  const Add = useCallback((id: number) => {
    setIDs((prevIDs) => [...prevIDs, id]); // the function form will receive the previous state as the argument.
  }, []);

Upvotes: 1

Related Questions