Passing a function with React Context API to child component nested deep in the tree to update state value

I’m using React Context API For the first time. I have a JWT Token Generator that takes inputs for header, payload, and secret. With this in mind, the JWT Generator Component lives nested down within a few different components. After the JWT is generated, I want to update the App so that the JWT is available throughout all different pages.

Based on previous work, I’ve tried to implement this using various forms of React Context but am specifically running into issues when attempting to pass down a sort of ‘setJWT’ function to the JWT Generator so that this state value is updated when a user is successfully authenticated.

App.tsx (parent)

export default class App extends Component<AppProps> {
  constructor(props: AppProps) {
    super(props);
    this.state = {};
  }

  render(): JSX.Element {
    // const { user } = this.state;

    return (
      <>
        <BrowserRouter>
          <Header />
          <Box className="root">
            <div style={{ display: 'flex' }}>
              <Box className="nav">
                <NavBar headings={headings} subheadings={subheadings} />
              </Box>
              <Box
                style={{
                  marginLeft: '210px',
                  marginTop: '50px',
                  position: 'fixed',
                }}
              >
                <Content />
              </Box>
            </div>
          </Box>
        </BrowserRouter>
      </>
    );
  }
}

Authentication.tsx (child)

export interface PaneProps {
  headings: {
    key: number;
    name: string;
    pathname: string;
  }[];
  subheadings: {
    key: number;
    name: string;
    calls: string[];
  }[];
}

interface PageState {
  isLoggedIn: boolean;
}

export default class Authentication extends Component<unknown, PageState> {
  constructor(props: PaneProps) {
    super(props);
    this.state = {
      isLoggedIn: true,
    };
  }

  render(): JSX.Element {
    const { isLoggedIn } = this.state;
    return (
      <>
        <Box className="outer-box">
          <Grid container className="grid-container">
            <div className="some-page-wrapper">
              <div className="row">
                <div className="column">
                  <div className="text-column">
                    <AuthenticationText />
                  </div>
                </div>

                {(function () {
                  if (isLoggedIn) {
                    return (
                      <div className="column">
                        <div className="text-column">
                          <div className="sticky-div-bs">
                            <JWTGenerator />
                          </div>
                        </div>
                      </div>
                    );
                  }
                  return (
                    <div className="column">
                      <div className="bs-column">
                        <div className="sticky-div-bs">
                          <IFrameBS />
                        </div>
                      </div>
                    </div>
                  );
                })()}
              </div>
            </div>
          </Grid>
        </Box>
      </>
    );
  }
}

JWTGenerator.tsx (child-child)

export interface AppProps {
  headings?: {
    key: number;
    name: string;
    pathname: string;
  }[];
  subheadings?: {
    key: number;
    name: string;
    calls: string[];
  }[];
}

interface AppState {
  header: string;
  payload: string;
  secret: string;
  textAreaValue?: string;
}

export default class JWTGenerator extends Component<AppProps, AppState> {
  constructor(props: AppProps) {
    super(props);
    this.state = {
      header: JSON.stringify(
        {
          alg: 'HS256',
          typ: 'JWT',
        },
        null,
        2,
      ),
      payload: JSON.stringify(
        {
          iat: 1501768003,
          iss: 'periodicdev',
          sub: 'periodicadmin',
        },
        null,
        2,
      ),
      secret: '',
      textAreaValue: '',
    };
    this.onChangeHeader = this.onChangeHeader.bind(this);
    this.onChangePayload = this.onChangePayload.bind(this);
    this.onChangeSecret = this.onChangeSecret.bind(this);
  }

  onChangeHeader(newHeader: string): void {
    this.setState({ header: newHeader });
  }

  onChangePayload(newPayload: string): void {
    this.setState({ payload: newPayload });
  }

  onChangeSecret(newSecret: string): void {
    this.setState({ secret: newSecret });
  }

  onChangeTextArea(header: string, payload: string, secretKey: string): void {
    const cleanHeader = btoa(header.replace(/s/g, ''));
    const cleanPayload = btoa(payload.replace(/=/g, '').replace(/s/g, ''));
    const baseMessage = `${cleanHeader}.${cleanPayload.replace(/=/g, '')}`;
    const secret = CryptoJS.HmacSHA256(baseMessage, secretKey)
      .toString(CryptoJS.enc.Base64)
      .replace(/=/g, '')
      .replace(/+/g, '-')
      .replace(///g, '_');
    this.setState({
      textAreaValue: `${cleanHeader}.${cleanPayload.replace(
        /=/g,
        '',
      )}.${secret}`,
    });
  }

  render(): JSX.Element {
    const { header, payload, secret, textAreaValue } = this.state;
    return (
      <>
        <div className="main-container-jwt">
          <div className="top-block-jwt">
            <Box className="top-bar-text-jwt">
              <i className="white">Example Call</i>
            </Box>
          </div>

          <div className="example-block-jwt">
            <div className="header-example-jwt">
              <AceEditor
                placeholder="Header"
                value={header}
                onChange={(e) => this.onChangeHeader(e)}
                onInput={(e: any) =>
                  this.onChangeTextArea(header, payload, secret)
                }
                // mode="json"
                theme="xcode"
                name="ace-editor"
                fontSize={11}
                showPrintMargin
                wrapEnabled
                showGutter
                setOptions={{
                  highlightGutterLine: false,
                  highlightActiveLine: false,
                  enableBasicAutocompletion: true,
                  enableLiveAutocompletion: true,
                  enableSnippets: false,
                  showLineNumbers: false,
                  tabSize: 2,
                  useWorker: false,
                }}
                style={{
                  color: 'red',
                  position: 'relative',
                  width: '100%',
                  height: '100%',
                  maxHeight: '100px',
                }}
              />
            </div>
            <div className="payload-example-jwt">
              <AceEditor
                placeholder="Payload"
                value={payload}
                onChange={(e: any) => this.onChangePayload(e)}
                onInput={(e: any) =>
                  this.onChangeTextArea(header, payload, secret)
                }
                // mode="json"
                theme="xcode"
                name="ace-editor"
                fontSize={11}
                showPrintMargin
                wrapEnabled
                showGutter
                setOptions={{
                  highlightGutterLine: false,
                  highlightActiveLine: false,
                  enableBasicAutocompletion: true,
                  enableLiveAutocompletion: true,
                  enableSnippets: false,
                  showLineNumbers: false,
                  tabSize: 2,
                  useWorker: false,
                }}
                style={{
                  color: 'blue',
                  position: 'relative',
                  width: '100%',
                  height: '100%',
                  maxHeight: '100px',
                }}
              />
            </div>
            <div className="secret-example-jwt">
              <AceEditor
                placeholder="Secret"
                value={secret}
                onChange={(e: any) => this.onChangeSecret(e)}
                onInput={(e: any) =>
                  this.onChangeTextArea(header, payload, secret)
                }
                // mode="json"
                theme="xcode"
                name="ace-editor"
                fontSize={11}
                showPrintMargin
                wrapEnabled
                showGutter
                setOptions={{
                  highlightGutterLine: false,
                  highlightActiveLine: false,
                  enableBasicAutocompletion: true,
                  enableLiveAutocompletion: true,
                  enableSnippets: false,
                  showLineNumbers: false,
                  tabSize: 2,
                  useWorker: false,
                }}
                style={{
                  color: 'green',
                  position: 'relative',
                  width: '100%',
                  height: '100%',
                  maxHeight: '100px',
                }}
              />
            </div>

            <div className="spacer">
              <Button
                // onClick={() =>
                //   this.setState((prev) => ({
                //     ...prev,
                //     textAreaValue: prev.value,
                //   }))
                // }
                onClick={(e: any) =>
                  this.onChangeTextArea(header, payload, secret)
                }
                className="try-it-button"
                style={{
                  backgroundColor: '#533cf8',
                  color: 'white',
                  borderRadius: 0,
                  fontSize: 13,
                  fontWeight: 200,
                }}
              >
                Make Call
              </Button>
              <div className="spacer-text-div">
                auto-update 'fat', alphabetize payload, and make the example
                call above
              </div>
            </div>

            <div className="header-2-jwt">
              <i className="white">Example Response</i>
            </div>

            <div className="textarea-example-jwt">
              <AceEditor
                // placeholder="Enter a call here..."
                value={textAreaValue || ''}
                readOnly
                // mode="json"
                theme="twilight"
                name="ace-editor"
                fontSize={11}
                showPrintMargin
                wrapEnabled
                setOptions={{
                  highlightGutterLine: false,
                  highlightActiveLine: false,
                  enableBasicAutocompletion: true,
                  enableLiveAutocompletion: true,
                  enableSnippets: false,
                  showLineNumbers: false,
                  tabSize: 2,
                  useWorker: false,
                  indentedSoftWrap: false,
                  wrapEnabled: true,
                }}
                style={{
                  position: 'relative',
                  width: '100%',
                  height: '100%',
                }}
              />
            </div>

            <div className="bottom-block-jwt" />
          </div>
        </div>
      </>
    );
  }
}

What I need to be able to do is to pass this jwt token throughout my App to ensure authentication for API users. Any suggestions/help is greatly appreciated. Thanks.

Answer

To do this, you will need to initialize some state at the top level of your app to set the JWT with. Of course, this state will be null at first, because as you said, you need to set the JWT a few children down. But you still need to initialize it at the top to use the Context API, because of how the context API works.

First create some user context:

import { createContext } from 'react'; 

export const UserContext = createContext(null);

Then create some state in App.js and put it in the context so it can be passed to the child that will update the JWT. Be sure to pass the setJwt function from useState.

const [jwt, setJwt] = useState(null);

    return (
     <UserContext.Provider value={{ jwt, setJwt }}>
        <BrowserRouter>
          <Header />
          <Box className="root">
            // etc. your app here
          </Box>
        </BrowserRouter>
     </UserContext.Provider>)

Then you can access that function in JWTGenerator.tsx (child-child) by retrieving it from context.

const { setJwt } = useContext(UserContext)

This will allow you to set the JWT in this child. And because the JWT was also passed in the context, that state value will be accessible in any other child components as soon as it updates in JWTGenerator.tsx.

This explains how to set user in context in detail with this pattern, although the user is set at the top level in this example, it is still the same pattern: https://jawblia.medium.com/react-template-for-jwt-authentication-with-private-routes-and-redirects-f77c488bfb85 I would also rec the React docs of course, and Ben Awad has a video tutorial about this on YouTube.