Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

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

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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 };

>Solution :

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.
  }, []);
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading