Search code examples
reactjsamazon-web-servicesreact-hooksaws-amplifygraphql-subscriptions

Access latest value of state in aws amplify subscriptions react


I have a useEffect hook with a subscription listener using aws-amplify like this.

useEffect(() => {
        todoUpdatedSubs.subscribe({
          next: (status: SubscriptionStatus<OnTodoUpdatedSubscription>) => {
            setIsTaskUpdated(true);
            // when mutation will run the next will trigger
            console.log("New Updated SUBSCRIPTION ==> ", status);
            if (!status.value.data.onTodoUpdated.success)
              return console.log("error", status);
    
            const newArr = [...todos];
            console.log("Prev todos", todos);
            const { id, todo, isCompleted } = status.value.data.onTodoUpdated;
            newArr[editingIndex] = { id, todo, isCompleted };
            setTodos(newArr);
          },
        });}, [])

The problem is that the next function in useEffect does not have the latest value of todo state because the useEffect only ran on mount. If I do something like this, multiple listeners are mounted:

useEffect(() => {
        todoUpdatedSubs.subscribe({
          next: (status: SubscriptionStatus<OnTodoUpdatedSubscription>) => {
            setIsTaskUpdated(true);
            // when mutation will run the next will trigger
            console.log("New Updated SUBSCRIPTION ==> ", status);
            if (!status.value.data.onTodoUpdated.success)
              return console.log("error", status);
    
            const newArr = [...todos];
            console.log("Prev todos", todos);
            const { id, todo, isCompleted } = status.value.data.onTodoUpdated;
            newArr[editingIndex] = { id, todo, isCompleted };
            setTodos(newArr);
          },
        });}, [todos])

How can I get the latest value of state in the listener?


Solution

  • Use the functional updates option of the useState hook. When setting the state using a callback, the callback is called with the previous values. This saves you the need to define todos as a dependency of the useEffect.

    useEffect(() => {
      todoUpdatedSubs.subscribe({
        next: (status: SubscriptionStatus<OnTodoUpdatedSubscription>) => {
          setIsTaskUpdated(true);
          // when mutation will run the next will trigger
          console.log("New Updated SUBSCRIPTION ==> ", status);
          if (!status.value.data.onTodoUpdated.success)
            return console.log("error", status);
            
          setTodos(todos => {
            const newArr = [...todos];
            const { id, todo, isCompleted } = status.value.data.onTodoUpdated;
            newArr[editingIndex] = { id, todo, isCompleted };
            
            return newArr;
          });
        },
    });}, [])
    

    If you need to use the updated todos value inside useEffect, but not when updating the state, you can use useRef instead.

    In your case I would use the functional updates option, but I'm adding this as an example:

    const todosRef = useRef();
    
    useEffect(() => {
      todosRef.current = todos;
    });
    
    useEffect(() => {
      todoUpdatedSubs.subscribe({
        next: (status: SubscriptionStatus<OnTodoUpdatedSubscription>) => {
          setIsTaskUpdated(true);
          // when mutation will run the next will trigger
          console.log("New Updated SUBSCRIPTION ==> ", status);
          if (!status.value.data.onTodoUpdated.success)
            return console.log("error", status);
            
          const newArr = [...todosRef.current];
          const { id, todo, isCompleted } = status.value.data.onTodoUpdated;
          newArr[editingIndex] = { id, todo, isCompleted };
          setTodos(newArr);
        },
    });}, [])