Search code examples
reactjsreact-hooksuse-effect

useEffect to rerender this component


I am trying to use useEffect to rerender postList (to make it render without the deleted post) when postsCount change, but I can't get it right. I tried to wrap everything inside useEffect but I couldn't execute addEventListener("click", handlePost) because I am using useEffect to wait for this component to mount first, before attaching the evenListener.

Parent component:

function Tabs() {
  const [posts, setPosts] = useState([]);
  const dispatch = useDispatch();

  const postsCount = useSelector((state) => state.posts.count);

  useEffect(() => {
    document.getElementById("postsTab").addEventListener("click", handlePost);
  }, [handlePost]);

  const handlePost = async (e) => {
    const { data: { getPosts: postData }} = await refetchPosts();
    setPosts(postData);
    dispatch(postActions.getPostsReducer(postData));
  };

  const { data: FetchedPostsData, refetch: refetchPosts } = useQuery( FETCH_POSTS_QUERY, { manual: true });

  const [postList, setPostsList] = useState({});

  useEffect(() => {
    setPostsList(
      <Tab.Pane>
        <Grid>
          <Grid.Column>Title</Grid.Column>
          {posts.map((post) => (
              <AdminPostsList key={post.id} postId={post.id} />
            ))}
        </Grid>
      </Tab.Pane>
    );
    console.log("changed"); //it prints "changed" everytime postCount changes (or everytime I click delete), but the component doesn't remount
  }, [postsCount]);

  const panes = [
    { menuItem: { name: "Posts", id: "postsTab", key: "posts" }, render: () => postList }
  ];

  return (<Tab panes={panes} />);
}

child/AdminPostsList component:

function AdminPostsList(props) {
  const { postId } = props;
  const [deletePost] = useMutation(DELETE_POST_MUTATION, {variables: { postId } });
  const dispatch = useDispatch();

  const deletePostHandler = async () => {
    dispatch(postActions.deletePost(postId));
    await deletePost();
  };
  return (
    <>
        <Button icon="delete" onClick={deletePostHandler}></Button>
    </>
  );
}

The Reducers

const PostSlice = createSlice({
  name: "storePosts",
  initialState: {
    content: [],
    count: 0,
  },
  reducers: {
    getPostsReducer: (state, action) => {
      state.content = action.payload;
      state.count = action.payload.length
    },
    deletePost: (state, action) => {
      const id = action.payload
      state.content = current(state).content.filter((post) => (post.id !== id))
      state.count--
    }
  },
});

Solution

  • Ok, let's discuss what I did wrong for the future reader:

    1. There is no need to use this weird spaghetti
    useEffect(() => {
      document.getElementById("postsTab").addEventListener("click", handlePost);
    }, [handlePost]);
    const panes = [
      { menuItem: { name: "Posts", id: "postsTab", key: "posts" }, render: () => postList }
    ];
    

    for I could've used a <Menu.Item onClick={handleClick}>Posts</Menu.Item> to attach the onClick directly.

    1. I had to use useEffect to monitor posts dependency, but .map() will automatically update its content if the array I am mapping had any changes so there is no need to use it use useEffect in this context.

    2. I think I can use lifting state to setPosts from the child component and the change will trigger .map() to remap and pop the deleted element, but I couldn't find a way to so, so I am using a combination of redux (to store the posts) and useEffect to dispatch the posts to the store than I am mapping over the stored redux element, idk if this is the best approach but this is all I managed to do.

    3. The most important thing I didn't notice when I almost tried everything is, I must update apollo-cache when adding/deleting a post, by using proxy.readQuery this is how I did it

      const [posts, setPosts] = useState([]);
    
      const handlePosts = async () => {
        const { data: { getPosts: postData } } = await refetchPosts();
        setPosts(postData);
      };
    
      const handlePosts = async () => {
        const { data } = await refetchPosts();
        setPosts(data.getPosts);
      };
    
      // Using useEffect temporarily to make it work.
      // Will replace it with an lifting state when refactoring later.
      useEffect(() => {
        posts && dispatch(postsActions.PostsReducer(posts))
      }, [posts]);
    
      const [deletePost] = useMutation(DELETE_POST_MUTATION, {
        update(proxy) {
          let data = proxy.readQuery({
            query: FETCH_POSTS_QUERY,
          });
          // Reconstructing data, filtering the deleted post 
          data = { getPosts: data.getPosts.filter((post) => post.id !== postId) };
          // Rewriting apollo-cache
          proxy.writeQuery({ query: FETCH_POSTS_QUERY, data });
        },
        onError(err) {
          console.log(err);
        },
        variables: { postId },
      });
    
      const deletePostHandler = async () => {
        deletePost();
        dispatch(postsActions.deletePost(postId))
      };
    

    Thanks to @Anuj Panwar @Milos Pavlovic for helping out, kudos to @Cptkrush for bringing the store idea into my attention