[SOLVED] Setting state with React Context from child component

Issue

This Content is from Stack Overflow. Question asked by kinzito17

Hey y’all I am trying to use the Context API to manage state to render a badge where the it’s not possible to pass props. Currently I am trying to use the setUnreadNotif setter, but it seems because I am using it in a method that loops through an array that it is not working as expected. I have been successful updating the boolean when only calling setUnreadNotif(true/false); alone so I know it works. I have tried many other approaches unsuccessfully and this seems the most straight forward. My provider is wrapping app appropriately as well so I know its not that. Any help is greatly appreciated.

Here is my Context

import React, {
  createContext,
  Dispatch,
  SetStateAction,
  useContext,
  useState,
} from 'react';
import { getContentCards } from 'ThisProject/src/utils/braze';
import { ContentCard } from 'react-native-appboy-sdk';

export interface NotificationsContextValue {
  unreadNotif: boolean;
  setUnreadNotif: Dispatch<SetStateAction<boolean>>;
}

export const defaultNotificationsContextValue: NotificationsContextValue = {
  unreadNotif: false,
  setUnreadNotif: (prevState: SetStateAction<boolean>) => prevState,
};

const NotificationsContext = createContext<NotificationsContextValue>(
  defaultNotificationsContextValue,
);

function NotificationsProvider<T>({ children }: React.PropsWithChildren<T>) {
  const [unreadNotif, setUnreadNotif] = useState<boolean>(false);

  return (
    <NotificationsContext.Provider
      value={{
        unreadNotif,
        setUnreadNotif,
      }}>
      {children}
    </NotificationsContext.Provider>
  );
}

function useNotifications(): NotificationsContextValue {
  const context = useContext(NotificationsContext);
  if (context === undefined) {
    throw new Error('useUser must be used within NotificationsContext');
  }

  return context;
}

export { NotificationsContext, NotificationsProvider, useNotifications };

Child Component

export default function NotificationsPage({
  navigation,
}: {
  navigation: NavigationProp<StackParamList>;
}) {
  const [notificationCards, setNotificationCards] = useState<
    ExtendedContentCard[]
  >([]);
  const user = useUser();
  const { setUnreadNotif } = useNotifications();

  
  const getCards = (url: string) => {
    setUnreadNotif(false);
    if (url.includes('thisproject:')) {
      Linking.openURL(url);
    } else {
      navigation.navigate(ScreenIdentifier.NotificationsStack.id, {
        screen: ScreenIdentifier.NotificationsWebView.id,
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        params: {
          uri: `${getTrustedWebAppUrl()}${url}`,
          title: 'Profile',
        },
      });
    }

    getContentCards((response: ContentCard[]) => {
      response.forEach((card) => {
        if (card.clicked === false) {
          setUnreadNotif(true);
        }
      });
    });

    Braze.requestContentCardsRefresh();
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.contentContainer}>
        {notificationCards?.map((item: ExtendedContentCard) => {
          return (
            <NotificationCard
              onPress={getCards}
              key={item.id}
              id={item.id}
              title={item.title}
              description={item.cardDescription}
              image={item.image}
              clicked={item.clicked}
              ctaTitle={item.domain}
              url={item.url}
            />
          );
        })}
      </View>
    </ScrollView>
  );
}



Solution

An issue I see in the getContentCards handler is that it is mis-using the Array.prototype.findIndex method to issue unintended side-effects, the effect here being enqueueing a state update.

getContentCards((response: ContentCard[]) => {
  response.findIndex((card) => {
    if (card.clicked === false) {
      setUnreadNotif(true);
    }
    setUnreadNotif(false);
  });
});

What’s worse is that because the passed predicate function, e.g. the callback, never returns true, so each and every element in the response array is iterated and a state update is enqueued and only the enqueued state update when card.clicked === false evaluates true is the unreadNotif state set true, all other enqueued updates set it false. It may be true that the condition is true for an element, but if it isn’t the last element of the array then any subsequent iteration is going to enqueue an update and set unreadNotif back to false.

The gist it seems is that you want to set the unreadNotif true if there is some element with a falsey card.clicked value.

getContentCards((response: ContentCard[]) => {
  setUnreadNotif(response.some(card => !card.clicked));
});

Here you’ll see that the Array.prototype.some method returns a boolean if any of the array elements return true from the predicate function.

The some() method tests whether at least one element in the array
passes the test implemented by the provided function. It returns true
if, in the array, it finds an element for which the provided function
returns true; otherwise it returns false. It doesn’t modify the array.

So long as there is some card in the response that has not been clicked, the state will be set true, otherwise it is set to false.


This Question was asked in StackOverflow by kinzito17 and Answered by Drew Reese It is licensed under the terms of CC BY-SA 2.5. - CC BY-SA 3.0. - CC BY-SA 4.0.

people found this article helpful. What about you?