React-Redux: User Conditional GUI Elements

GitHub Repos

💾 Ncoughlin: React-Streams-Client

💾 Ncoughlin: React-Streams-API

Intro

Continuing our series on making a Twitch Clone called “Glitch”, at this point we have created actions and reducers for our stream CRUD operations. We are also listing all the currently created streams in a list on the homepage.

The next step is to make it so that streams can be edited or deleted. However we want to limit this ability to streams that the user has created themselves. To do that we are going to be changing the stream objects so that they include the Google ID of the user that created them. We can then perform a conditional check later on this ID to make the options to edit or delete only available to the author.a

Modifying Create Stream Action

To start with we need to modify our POST request to the database so that each new stream includes the users ID, and that POST request is assembled inside of the createStream action. Let’s head over to our actions index and change this up.

actions/index.js
import streams from "../apis/streams";
import {
  SIGN_IN,
  SIGN_OUT,
  FETCH_STREAMS,
  FETCH_STREAM,
  CREATE_STREAM,
  DELETE_STREAM,
  EDIT_STREAM,
} from "./types";

// USER AUTHENTICATION ACTIONS
// change user status to signed in
export const signIn = (userId) => {
  return {
    type: SIGN_IN,
    payload: userId,
  };
};

// change user status to signed out
export const signOut = () => {
  return {
    type: SIGN_OUT,
  };
};

// STREAMS CRUD REST ACTIONS
// fetch streams
export const fetchStreams = () => async (dispatch) => {
  const response = await streams.get("/streams");

  dispatch({ type: FETCH_STREAMS, payload: response.data });
};

// fetch stream
export const fetchStream = (streamId) => async (dispatch) => {
  const response = await streams.get(`/streams/${streamId}`);

  dispatch({ type: FETCH_STREAM, payload: response.data });
};

// create stream
export const createStream = (formValues) => async (dispatch, getState) => {  // retrieve user ID from store  const { userId } = getState().auth;  const response = await streams.post("/streams", {...formValues, userId});  dispatch({ type: CREATE_STREAM, payload: response.data });};
// update stream
export const editStream = (streamId, formValues) => async (dispatch) => {
  const response = await streams.put(`/streams/${streamId}`, formValues);

  dispatch({ type: EDIT_STREAM, payload: response.data })
};

// delete stream
export const deleteStream = (streamId) => async (dispatch) => {
  await streams.delete(`/streams/${streamId}`);

  dispatch({ type: DELETE_STREAM, payload: streamId })
};

And to break down a little bit what is happening here, let us start with adding the getState method to our thunk action. This is a Redux store method that returns the current state tree, and we then de-structured the current user ID out of that.

We then modified the post request so that we are passing in an object which is a copy of all of the formValues, with the userId appended to the end.

user id in JSON database

And we can see that the userId was added successfully to the database.

Passing User ID to Steam List Component As Prop

We are going to be comparing the ID of the current user to the ID of the user that created each stream, and use that comparison to conditionally render a set of edit and delete buttons. The first step in creating this comparison is passing the current user ID down to the StreamList component as a prop. So lets get that done.

components/StreamList.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchStreams } from "../../actions";

class StreamList extends Component {
  componentDidMount() {
    this.props.fetchStreams();
  }

  renderList() {
    return this.props.streams.map((stream) => {
      return (
        <div className="item" key={stream.id}>
          <i className="large middle aligned icon camera" />
          <div className="content">
            {stream.title}
            <div className="description">{stream.description}</div>
          </div>
        </div>
      );
    });
  }

  render() {
    return (
      <div>
        <h2>Streams</h2>
        <div className="ui celled list">{this.renderList()}</div>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return { 
    streams: Object.values(state.streams),
    currentUserId: state.auth.userId  };
};

export default connect(mapStateToProps, { fetchStreams })(StreamList);

That was an easy one, we just need to expand the map state to props object. Now we can create a helper function that does the comparison.

Conditional JSX Content

Now we can create a helper function that returns a block of JSX if the author id of a stream matches the current user id. We know that the stream id therefore must get passed into this helper function as an argument, as it is going to change every time we run this checker function.

components/streams/StreamList.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchStreams } from "../../actions";

class StreamList extends Component {
  componentDidMount() {
    this.props.fetchStreams();
  }

  // check if stream author is current user
  renderAdmin(stream) {    if (stream.userId === this.props.currentUserId) {      return (        <div className="right floated content">          <button className="ui button primary">Edit</button>          <button className="ui button negative">Delete</button>        </div>      );    }  }
  renderList() {
    return this.props.streams.map((stream) => {
      return (
        <div className="item" key={stream.id}>
          {this.renderAdmin(stream)}          <i className="large middle aligned icon camera" />
          <div className="content">
            {stream.title}
            <div className="description">{stream.description}</div>
          </div>
        </div>
      );
    });
  }

  render() {
    return (
      <div>
        <h2>Streams</h2>
        <div className="ui celled list">{this.renderList()}</div>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    streams: Object.values(state.streams),
    currentUserId: state.auth.userId,
  };
};

export default connect(mapStateToProps, { fetchStreams })(StreamList);

and we can see that this checker function is correctly rendering our buttons only for the matching author.

conditional buttons working

Example 2: Conditional New Stream Button

Let’s say that we want to make it so that the “Create Stream” button is only visible to users who are signed in? We can do this easily following the exact same process.

This time in our checker function we want to use the isSignedIn auth state from the store, so we add that to our mapStateToProps function, create another helper function with our JSX and then place that in our render layout.

components/streams/StreamList.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";

import { fetchStreams } from "../../actions";

class StreamList extends Component {
  componentDidMount() {
    this.props.fetchStreams();
  }

  // check if stream author is current user
  renderAdmin(stream) {
    if (stream.userId === this.props.currentUserId) {
      return (
        <div className="right floated content">
          <button className="ui button primary">Edit</button>
          <button className="ui button negative">Delete</button>
        </div>
      );
    }
  }

  renderList() {
    return this.props.streams.map((stream) => {
      return (
        <div className="item" key={stream.id}>
          {this.renderAdmin(stream)}
          <i className="large middle aligned icon camera" />
          <div className="content">
            {stream.title}
            <div className="description">{stream.description}</div>
          </div>
        </div>
      );
    });
  }

  // show "Create Stream" button if user is signed in
  renderCreate() {    if (this.props.isSignedIn) {      return (        <div style={{ textAlign: "right" }}>          <Link to="/streams/new">            <button className="ui green basic button">Create Stream</button>          </Link>        </div>      );    }  }
  render() {
    return (
      <div>
        <h2>Streams</h2>
        <div className="ui celled list">{this.renderList()}</div>
        {this.renderCreate()}
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    streams: Object.values(state.streams),
    currentUserId: state.auth.userId,
    isSignedIn: state.auth.isSignedIn,  };
};

export default connect(mapStateToProps, { fetchStreams })(StreamList);

create stream button

We have also wrapped this new JSX button in a Link component to use React-Router to link to our CreateStream component.