skip to Main Content

What specific syntax needs to be changed in the code below in order for the value returned by the currentSession() function to correctly set the html that is rendered to the user in the web browser?

Specifically, what needs to be changed so that the console.log("other groups is: ", groups); line returns a valid array of groups, and so that the logic in the {isGroupOne ? <p>Welcome back!</p> : <p>Please log in.</p>} line is able to execute with isGroupOne having been returned as True?

USE CASE:

A React component needs to render different content for users that are in different user groups. The groups for a given user are indeed returned from a backend service.

PROBLEM:

The problem is that the React component is printing out the user’s groups as a pending promise instead of as a valid array of group names that can be transformed into a Boolean to indicate whether or not the user is in a specific group.

As a result, the wrong content is being printed to the web browser in the example broken code below. Currently, Please log in. is being printed in the browser, even though you can see from the logs below that the promise eventually resolves to give isGroupOne the value of true.

MINIMAL CODE TO REPRODUCE PROBLEM:

Here is the minimal code required to reproduce the problem:

import { fetchAuthSession } from 'aws-amplify/auth';

function Index() {
  let isGroupOne = false;

  async function currentSession() {
    try {
      const { accessToken, idToken } = (await fetchAuthSession()).tokens ?? {};
      const groups = accessToken["payload"]["cognito:groups"]
      console.log("these groups: ", groups);
      return groups;
    } catch (err) {
      console.log(err);
    }
  }

  const groups = currentSession().then(groups => {
    console.log("those groups is: ", groups);
    //iterate groups array to see if "GroupOne" is in the array
    groups.forEach(group => {
      if (group === "GroupOne") {
        console.log("User is in GroupOne group");
        isGroupOne = true;
        console.log("isGroupOne: ", isGroupOne);
      }
    });

  });
  console.log("other groups is: ", groups);

  return (
    <div>
        {isGroupOne ? <p>Welcome back!</p> : <p>Please log in.</p>}
    </div>

  );
}

export default Index;

LOGS ILLUSTRATING THE PROBLEM:

The following is printed to the console when the page defined by the above minimal code is rendered in the web browser:

other groups is:  Promise {<pending>}
index.js:29 other groups is:  Promise {<pending>}
index.js:10 these groups:  ['GroupOne']
index.js:18 those groups is:  ['GroupOne']
index.js:22 User is in GroupOne group
index.js:24 isGroupOne:  true
index.js:10 these groups:  ['GroupOne']
index.js:18 those groups is:  ['GroupOne']
index.js:22 User is in GroupOne group
index.js:24 isGroupOne:  true

2

Answers


  1. You can use React Hooks to achieve what you want – more specifically, useState and useEffect.

    useState helps you to store state that your UI components rely on – once the state is updated, these components will get updated as well.

    useEffect can be used to execute code that should be updated when some external dependency gets updated (the second parameter to the function, an empty array in this example). By using the empty array we’re stating this should only be executed once (since there’s no dependencies that would lead to this being run again).

    import { useEffect, useState } from "react";
    
    // Emulates retrieving the user from Cognito
    async function mockedFetchAuthSession(): Promise<{ groups: string[] }> {
      // adjust, save, and reload the page to see diff results
      const groups = ["GroupOne"]; // <-- replace with another group do see diff results
      return new Promise((resolve) => {
        setTimeout(() => resolve({ groups }), 2_000);
      });
    }
    
    function groupOne(group: string): boolean {
      return group === "GroupOne";
    }
    
    export default function TheIndex() {
      const [isGroupOne, setIsGroupOne] = useState<boolean | null>(null);
    
      async function currentSession() {
        try {
          const { groups } = await mockedFetchAuthSession();
          console.log("these groups: ", groups);
          setIsGroupOne(groups.some(groupOne));
        } catch (err) {
          console.log(err);
        }
      }
    
      useEffect(() => {
        currentSession();
      }, []);
    
      return (
        <div>
          {isGroupOne === null ? (
            <p>Loading...</p>
          ) : isGroupOne ? (
            <p>Welcome back!</p>
          ) : (
            <p>Please log in.</p>
          )}
        </div>
      );
    }
    
    

    Running code: https://codesandbox.io/s/xenodochial-fermi-4mth3h?file=/src/TheIndex.tsx

    Login or Signup to reply.
  2. It is a classic Frontend issue: rendering data that is asynchronously fetched from Backend.

    There is no magic, the only solution is to conditionally render a loading state while waiting for the Backend data, then once received, you can proceed as originally planned. Some Frontend frameworks or libraries may provide helpers to automatically manage this behavior, but it always implements the same scheme under the hood.

    For example in your case, using useAsync hook from react-use:

    function Index() {
      const groupsAsync = useAsync(currentSession, []); // Fill the dependency array if session should be refreshed on some criteria
    
      const isGroupOne = useMemo(() =>
        !groupsAsync.loading && 
        groupsAsync.value?.some(
          (group) => group === "GroupOne"
        ), [groupsAsync.loading, groupsAsync.value]);
    
      return (
        <div>
          {groupsAsync.loading
            ? "Loading..."
            : isGroupOne
            ? <p>Welcome back!</p>
            : <p>Please log in.</p>}
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search