Module 8: Converting to Context (Optional)

In this module, we'll convert our application to use the new in 16.3 Context API in React.

What is Context?

Context is new to React 16.3, though there was an older unstable version that's different. The context API allows for us to be able to access information in different parts of the application without having to pass props down multiple levels, or access the same information in sibling components. For example, in the application we just built, we have to pass down sessionToken and setSessionState multiple times in order for us to be able to use them throughout our application. The context API gives us a different way to access the same information.

The official docs on context: here.

Why Are We Using It?

The use case for context is pretty ideal in our application, we have things passed down multiple children and accessed in different areas of our application. We could do this more efficiently through context.

1. Setting up the AuthContext

Create a new file called AuthContext.js in your auth folder. Type the following code in AuthContext.js:

import React from 'react';
export const AuthContext = React.createContext({
    sessionToken: '',
    setToken: () => {},
  });

What we've just done is create a new Context called AuthContext. This has two pieces of information in here, a string called sessionToken which is currently empty, and a currently blank function called setToken.

2. Setting up the AuthContext Provider

The next step in switching to context, is create the provider for the context. We'll do this in App.js. Change your constructor in App.js to look exactly like this:

  constructor() {
    super();
    this.setToken = (token) => {
      localStorage.setItem('token', token);
      this.setState({ sessionToken: token });
    }

    this.state = {
      sessionToken: '',
      setToken: this.setToken
    }
  }

Notice how we've created a function called setToken in our constructor, which we then have in our state. This is so that we can pass it to our context provider more easily. Next, we'll create that provider. Make your App.js render look like this:

render() {
    return (
      <Router>
        <AuthContext.Provider value={this.state}>
        <div>
          <SiteBar clickLogout={this.logout} />
          {this.protectedViews()}
        </div>
        </AuthContext.Provider>
      </Router>
    );
  }

Notice that we've wrapped our application in our AuthContext Provider. This allows us to access the information contained within our AuthContext anywhere in our application. Also notice that we set the value of the Provider to the state of our App.js.

3. Consuming the AuthContext - Setting the Token

Now, consuming is where we use the information from the AuthContext in our application. The great thing about context, is we don't need to pass it down, we can just call it in the components that need it. While we're at it, let's go ahead and refactor. Delete the Splash.js file, and then in your App.js, replace where the <Splash /> component is with <WorkoutIndex />. Don't forget to import it. Your protectedViews function should look like this:

protectedViews = () => {
    if (this.state.sessionToken === localStorage.getItem('token')) {
      return (
        <Switch>
          <Route path='/' exact>
            <WorkoutIndex />
          </Route>
        </Switch>
      )
    } else {
      return (
        <Route path="/auth" >
          <Auth />
        </Route>
      )
    }
  }

Notice that we have not passed props to WorkoutIndex or Auth. This is because we no longer need to, and can use our AuthContext instead!

In order to be able to access our AuthContext inside of lifecycle methods and different functions, we can set our context to be the props of that component. Let's first see this in Login.js. Replace the export default Login with:

export default props => (
    <AuthContext.Consumer>
      {auth => <Login {...props} auth={auth} />}
    </AuthContext.Consumer>
  );

What we've just done is allowed our Login component to use the information from our AuthContext as one of its props. Notice that we've included the rest of the props with {...props} so that we don't lose anything.

So to actually use information from our context, in our handleSubmit function we're going to call the function we need from this.props.auth instead of this.props. This is how the function should look

handleSubmit = (event) => {
    fetch("http://localhost:3000/api/login", {
        method: 'POST',
        body: JSON.stringify({user:this.state}),
        headers: new Headers({
            'Content-Type': 'application/json'
            })
    }).then(
        (response) => response.json()
    ).then((data) => {
        this.props.auth.setToken(data.sessionToken) //what we changed is here!
    }) 
    event.preventDefault()
}

Notice how we only changed one line. So instead of this.props, it's just this.props.auth because that's what we set our context to! How cool is that, no need to pass our function down forever, we can just call it where we need it from our context!

Next, we'll do essentially the same thing in Signup.js. The completed code should look like this:

import React, { Component } from "react";
import { Button, Form, FormGroup, Label, Input } from 'reactstrap';
import {AuthContext}  from '../auth/AuthContext'

class Signup extends Component {
    constructor(props) {
        super(props)
        this.state = {
            username: '',
            password: ''
        };
    }

    handleChange = (event) => {
        this.setState({
            [event.target.name]: event.target.value,
        });
    }

    handleSubmit = (event) => {
        fetch("http://localhost:3000/api/user", {
            method: 'POST',
            body: JSON.stringify({user:this.state}),
            headers: new Headers({
                'Content-Type': 'application/json'
              })
        }).then(
            (response) => response.json()
        ).then((data) => {
            this.props.auth.setToken(data.sessionToken) //again this.props.auth to use our function from AuthContext!
        }) 
        event.preventDefault()
    }

    validateSignUp = (event) => {
        this.setState({
            errorMessage:'Fields must not be empty'
        })
        event.preventDefault();
    }

    render() {
        const submitHandler = !this.state.username ? this.validateSignUp : this.handleSubmit
        return (
            <div>
                <h1>Sign Up</h1>
                <h6>Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus repellat, atque nulla, soluta vero reprehenderit numquam incidunt, rem quaerat quos voluptatum perferendis. Distinctio culpa iste atque blanditiis placeat qui ipsa?</h6>
                <Form onSubmit={submitHandler} >
                    <FormGroup>
                        <Label for="username">username</Label>
                        <Input id="username" type="text" name="username" placeholder="enter username" onChange={this.handleChange} />
                        {this.state.errorMessage && <span className="error">user name is required</span>}
                    </FormGroup>
                    <FormGroup>
                        <Label for="password">Password</Label>
                        <Input id="su_password" type="password" name="password" placeholder="enter password" onChange={this.handleChange} />
                    </FormGroup>
                    <Button type="submit"> Submit </Button>
                </Form>
            </div>
        )
    }
}
// this is where we use the context to set our AuthContext info the auth prop
export default props => (
    <AuthContext.Consumer>
      {auth => <Signup {...props} auth={auth} />}
    </AuthContext.Consumer>
);

Take a look at the comments above as a reminder of what we changed!

Consuming the Context - Using the SessionToken

Next is the other half of our context, the sessionToken! First thing, is anywhere we have previously passed sessionToken or token as a prop we can take out! So in our WorkoutIndex we no longer have to pass the token to everything! Next, let's go to WorkoutIndex and consume our AuthContext there!

Replace the export default WorkoutIndex with:

export default props => (
  <AuthContext.Consumer>
    {auth => <WorkoutIndex {...props} auth={auth} />}
  </AuthContext.Consumer>
);

Just like before we're setting our auth info as a prop so that we can use it easily in our WorkoutIndex.

Next, we're going to change wherever we used our token in our API call methods to be this.props.auth.sessionToken. Your methods should like the following:

  fetchWorkouts = () => {
    fetch("http://localhost:3000/api/log", {
      method: 'GET',
      headers: new Headers({
        'Content-Type': 'application/json',
        'Authorization': this.props.auth.sessionToken //using it from context
      })
    })
      .then((res) => res.json())
      .then((logData) => {
        return this.setState({ workouts: logData })
      })
  }

  workoutDelete = (event) => {
    fetch(`http://localhost:3000/api/log/${event.target.id}`, {
      method: 'DELETE',
      body: JSON.stringify({ log: { id: event.target.id } }),
      headers: new Headers({
        'Content-Type': 'application/json',
        'Authorization': this.props.auth.sessionToken //from context
      })
    })
      .then((res) => this.fetchWorkouts())
  }

  workoutUpdate = (event, workout) => {
    fetch(`http://localhost:3000/api/log/${workout.id}`, {
      method: 'PUT',
      body: JSON.stringify({ log: workout }),
      headers: new Headers({
        'Content-Type': 'application/json',
        'Authorization': this.props.auth.sessionToken //from context
      })
    })
      .then((res) => {
        this.setState({ updatePressed: false })
        this.fetchWorkouts();
      })
  }

We're going to do similar things for WorkoutCreate! Check out the finished code:

import React, { Component } from 'react';
import { Button, Form, FormGroup, Label, Input } from 'reactstrap';
import {AuthContext}  from '../auth/AuthContext'

class WorkoutCreate extends Component {
    constructor(props) {
        super(props)
        this.state = {
            result: '',
            description: '',
            def: ''
        };
    }

    handleChange = (event) => {
        this.setState({
            [event.target.name]: event.target.value
        })
    }

    handleSubmit = (event) => {
        event.preventDefault();
        fetch(`http://localhost:3000/api/log/`, {
            method: 'POST',
            body: JSON.stringify({ log: this.state }),
            headers: new Headers({
                'Content-Type': 'application/json',
                'Authorization': this.props.auth.sessionToken // from our context!
            })
        })
            .then((res) => res.json())
            .then((logData) => {
                this.props.updateWorkoutsArray()
                this.setState({
                    result: '',
                    description: '',
                    def: ''
                })
            })
    }

    render() {
        return (
            <div>
                <h3>Log a Workout</h3>
                <hr />
                <Form onSubmit={this.handleSubmit} >
                    <FormGroup>
                        <Label for="result">Result</Label>
                        <Input id="result" type="text" name="result" value={this.state.result} placeholder="enter result" onChange={this.handleChange} />
                    </FormGroup>
                    <FormGroup>
                        <Label for="def">Type</Label>
                        <Input type="select" name="def" id="def" value={this.state.def} onChange={this.handleChange} placeholder="Type">
                            <option></option>
                            <option value="Time">Time</option>
                            <option value="Weight">Weight</option>
                            <option value="Distance">Distance</option>
                        </Input>
                    </FormGroup>
                    <FormGroup>
                        <Label for="description">Notes</Label>
                        <Input id="description" type="text" name="description" value={this.state.description} placeholder="enter description" onChange={this.handleChange} />
                    </FormGroup>
                    <Button type="submit" color="primary"> Submit </Button>
                </Form>
            </div>
        )
    }
}
//using authcontext and setting it to the prop called auth!
export default props => (
    <AuthContext.Consumer>
      {auth => <WorkoutCreate {...props} auth={auth} />}
    </AuthContext.Consumer>
);

Hopefully, you noticed how using context can alleviate the need to pass things down multiple levels to multiple different components. It's pretty neat how we can access information from a much removed component using Context. For more explanation and examples don't forget to check out the React Official docs: here!!

Last updated