Search code examples
react-nativeuse-effectreact-native-firebase

How to trigger a useEffect from external function?


I'm new to both Javascript and React Native and I need some help working out how I can trigger a useEffect from an external function.

My application is listening for messages from Firebase Cloud Messaging (FCM) with the react-native-firebase library. On receipt, a background handler writes the message to asyncstorage and then displays a notification with notifee library.

When the application is opened, it retrieves the messages from asyncstorage and renders them using FlatList. This works perfectly when the application is first opened and the list renders.

The problem is that if a message is received while the app is in the foreground, the handler correctly processes the incoming message and displays the notification, but it cannot trigger a refresh of the message list which has already been rendered.

How can I trigger a refresh/re-run of the useEffect from the foregroundMessageHandler?

This is the async function I'm using to capture the FCM message while the app is in the foreground:

index.js

const foregroundMessageHandler = messaging().onMessage(async remoteMessage => {

  await _handleNewMessage(remoteMessage);  // Inserts FCM message into asyncstorage

  await onDisplayNotification(remoteMessage);  // Displays notification

  // ...how to trigger a refresh of the useEffect from here??

});

And this is how I'm rendering the messages from asyncstorage in the UI:

app.js

function HomeScreen() {

  const [isLoading, setLoading] = useState(true);
  const [data, setData] = useState(false);

  useEffect(() => {

      // Function to retrieve historical messages from AsyncStorage
      async function _retrieveData() {
        try {
          const value = await AsyncStorage.getItem('messages');
          if (value !== null) {

            const oldMessages = JSON.parse(value);

            setData(oldMessages);
            setLoading(false);

          } else {

            setData(false);
            setLoading(false);

          }
        } catch (error) {}
      }

      _retrieveData();  // Retrieve old messages from asyncstorage

  },[]);



function renderMessagesFlatList(data) {

  if (isLoading) {

    return (
      <ActivityIndicator />
    );

  } else if (data) {

  return (

  <FlatList
      data={data}
      keyExtractor={(item, index) => item.id}
      renderItem={({ item }) => (
          <>
          <View style={{ flexDirection: 'row', flex: 1 }}>
          <Image
            style={{ width: 40, height: 40 }}
            source={{uri:item.icon}}
          />
            <View style={{ flex: 1, paddingLeft: 5 }}>
            <Text style={{ color: '#000000', fontWeight: 'bold' }}>{item.sender}</Text>
            <Text>{timeSince(item.time)} ago</Text>
            </View>
          </View>
          <Text style={{ paddingTop: 4 }}>{item.body}</Text>
          <Divider style={{ backgroundColor: '#e7e5e5', height: 2, marginTop: 5, marginBottom: 5 }} />
          </>
        )}
    />
  );

  } else {

    return (
    <Text>No notifications to display</Text>
    );

  };

};

Update: Using the code below I've managed to refresh the FlatList component when a message is received. But it seems every time the useEffect fires it creates another foreground listener which results in duplicate notifications for subsequent messages. I need to work out how to stop these duplicated foreground listeners from being created.

function HomeScreen() {

const [data, setData] = useState([]);
const [refreshPage, setRefreshPage] = useState(false);

useEffect(async () => {

  const value = await AsyncStorage.getItem('messages35');

  // ...functions to retrieve and sort historical messages into array "sortedInput"

  setData(sortedInput);  // setState to provide initial data for FlatList component

  };

  const unsubscribeOnMessage = messaging().onMessage(async remoteMessage => {

    // Function to process new message and insert into local storage
    await _handleNewMessage(remoteMessage);

    // Display notification to user
    onDisplayNotification(remoteMessage);

    // Trigger refresh of FlatList component with setState
    setRefreshPage(Math.random() * 100);

  });

  return () => {
    unsubscribeOnMessage();
  };

}, [refreshPage]);

Solution

  • There's multiple solutions for your problem, but here's how I would prefer to solve it.

    You can add another messaging().onMessage listener to your HomeScreen component and whenever a new message is received, you simply refresh your list by retrieving the data again:

    import messaging from '@react-native-firebase/messaging';
    
    function HomeScreen() {
      // ...
    
      useEffect(() => {
        // Function to retrieve historical messages from AsyncStorage
        async function _retrieveData() {
          try {
            const value = await AsyncStorage.getItem('messages');
            if (value !== null) {
              const oldMessages = JSON.parse(value);
    
              setData(oldMessages);
              setLoading(false);
            } else {
              setData(false);
              setLoading(false);
            }
          } catch (error) {}
        }
    
        _retrieveData(); // Retrieve old messages from asyncstorage
    
        // Foreground push notifications listener
        const unsubscribeOnMessage = messaging().onMessage(() => {
          // Whenever a new push notification is received, call the _retrieveData function again
          _retrieveData();
        });
    
        return () => {
          unsubscribeOnMessage();
        };
      }, []);
    
      // ...
    }
    

    UPDATE

    1. You can leave the foregroundMessageHandler out of your HomeScreen component (your initial code). Having multiple messaging.onMessage listeners is valid.
    2. Instead of having const [refreshPage, setRefreshPage] = useState(false); you can just use setData in your messaging.onMessage listener in HomeScreen component. So the solution could be something like this:

    index.js

    const foregroundMessageHandler = messaging().onMessage(async remoteMessage => {
    
      await _handleNewMessage(remoteMessage);  // Inserts FCM message into asyncstorage
    
      await onDisplayNotification(remoteMessage);  // Displays notification
    
      // ...how to trigger a refresh of the useEffect from here??
    
    });
    

    app.js

    import messaging from '@react-native-firebase/messaging';
    
    function HomeScreen() {
      // ...
      const [isLoading, setLoading] = useState(true);
      const [data, setData] = useState(false);
    
      useEffect(() => {
        // ... Other code
       
        const unsubscribeOnMessage = messaging().onMessage((remoteMessage) => {
          // use setData to update your data for FlatList. It could be used to just append the new message to the current data
          setData((data) => [...data, remoteMessage]);
        });
    
        return () => {
          unsubscribeOnMessage();
        };
        // There shouldn't be any dependencies here
      }, []);
    
      // ...
    }