Dynamic profile image using React hooks and Context API

blogreactreact context apireact hooks#Import 2023-08-04 15:48

Dynamic profile image using React hooks and Context API

When i started redesigning scotchy.co I wanted the user interface to feel fluid and dynamic without a lot of obvious page loads.  React obviously allows you to do this with state very easily, but it can be quite cumbersome to manage the state throughout the different layers of nested components. This is where React Hooks come into play.

React Hooks allow you to access a certain state, like the authenticated user object, throughout your application without needed to keep passing it down from the top level.  This means that when you want to, for example, change your profile image, you can push that change to other components that are subscribing to that same set of data through another great feature called Context. Lets walk through an example.

Lets say I have this component in my header to show a users profile image like this

import React from 'react';
import { Link } from 'react-router-dom';
import { Row, Col } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

const LoggedInLinks = ({user}) => {
    return (
        <Row>
            <Col id="header-links" className="col-8">
                <Link to="/bottle/new">add</Link>
            </Col>
            <Col id="header-profile" className="col-4">
                <Link to="/profile" alt="profile user.avatar" className="float-right">
                    <FontAwesomeIcon icon={['fad', user.avatar]} size="2x" />
                </Link>
            </Col>
        </Row>
    )
}

export default LoggedInLinks;

and a profile section where a user can update their avatar from a set of FontAwesome user icons like this

import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Row, Col } from 'react-bootstrap';

import Input from '../../shared/forms/Input';
import SubmitButton from '../../shared/forms/SubmitButton';

const UpdateAvatar = ({user, submitCallback}) => {
  const setAvatar = () => {
     ...save to server...
  }
  
  return (
    <form onSubmit={submitCallback}>
        <div className="input-group input-group-lg scotchy-input">
            <Row>
                <Col xs={1} s={1} m={3} lg={3}>
                    <span id="current-avatar">
                        <FontAwesomeIcon icon={['fad', user.avatar]} size="8x" />
                    </span>
                </Col>
                <Col xs={12} s={12} m={9} lg={9}>
                    <div id="avatar-options">
                        <p>Choose your avatar</p>
                        <span data-icon="user-crown" className={user.avatar === 'user-crown' ? `selected avatar-option` :`avatar-option`} onClick={() => setAvatar('user-crown')}>
                            <FontAwesomeIcon icon={['fad', 'user-crown']} size="3x" />
                        </span>
                        <span data-icon="user-visor" className={user.avatar === 'user-visor' ? `selected avatar-option` :`avatar-option`} onClick={e => setAvatar('user-visor')}>
                            <FontAwesomeIcon icon={['fad', 'user-visor']} size="3x" />
                        </span>
                        <span data-icon="user-ninja" className={user.avatar === 'user-ninja' ? `selected avatar-option` :`avatar-option`} onClick={e => setAvatar('user-ninja')}>
                            <FontAwesomeIcon icon={['fad', 'user-ninja']} size="3x" />
                        </span>
                    </div>
                </Col>
            </Row>
        </div>
        <SubmitButton value="SAVE" />
    </form>
  );
};

export default UpdateAvatar;

Now if I want to have that change show in my header right after, i have a few options.

  1. I can refresh the page, but this does not have the effect i want in a fluid interface, and feels clunky as a user requiring all data to be reloaded.
  2. I can set a global state and push that state down through the component tree in both directions, but this requires each parent of both of these small components to know about the user object and its callbacks.
  3. I can use a Context.  React context api allows you to set create an objects and callbacks that can be refrenced dynamically throughout your code without them needing to be pushed through the component tree.  This allows us to create a dynamice and fluid user experience.  Let see how it works.

First we will need to create our context. Lets create a new file called AvatarContext.jsx and it will look like this

import { createContext } from "react";

export const AvatarContext = createContext({
  avatar: 'user',
  setAvatar: () => {},
});

here we set a default name of user which provided an avatar for unauthenticated users.  Now in the header component from the beginning we can add

...

import { UserContext } from './UserContext';

const LoggedInLinks = () => {
    const user = useContext(UserContext);
    ...
}

Instead of having to pass the user into the component.  This component lives in /src/components/shared/LoggedInLinks.jsx so its ideal to now have to pass the user through all of its parents from app.js

Now we can change our form to actually update the context

...
import { UserContext } from '../../shared/UserContext';

const ProfileForm = ({submitCallback}) => {
    // Retrieve context data
  const user = useContext(UserContext);
 
  return (
    ...
    <span data-icon="user-visor" className={user.avatar === 'user-visor' ? `selected avatar-option` :`avatar-option`} onClick={() => user.setAvatar('user-visor')}>
       <FontAwesomeIcon icon={['fad', 'user-visor']} size="3x" />
     </span>
   )
}

The key here is the user.setAvatar(...) which is the callback we set in our context, now when we change our Avatar on the profile page, our header automatically updates.