Building My Personal Portfolio with React & Redux (pt.2)

jacquelinelam

Jacqueline Lam

Posted on July 15, 2020

Building My Personal Portfolio with React & Redux (pt.2)

In part 1 of my article, I discussed why I decided to rebuild my personal portfolio with a Rails API and React & Redux front-end, and touched on the set up of my application.

In part 2, we will take a look at the features that I built and how they work based on the Redux Flow.

Feature Highlights

Filtering Projects By Stacks

Some research show that “6 seconds is the average time recruiters spent reading a resume”. With that in mind, I tried to design a portfolio website with a simple UI and features that will keep users engaged, and focused on the most important visual elements.

For a full-stack software engineer role, one of the most important things recruiters ask is “does the candidate have any experience using ‘xyz’ language or frameworks?” What that in mind, I designed the portfolio website with a simple filter bar so any visitor can see exactly which projects correspond to which sets of selected technologies.

When the user presses a filter button, it will trigger an onClick event, calling the addFilter or removeFilter callback prop (line 34 and line 39), based on the current state of the button (the button state is handled in my local React state.)

1 import React, { Component } from 'react'
2 
3 class FilterButton extends Component {
4  state = {
5    selected: undefined
6  }
7
8  componentDidMount() {
9    const { selectedStackIds, stack } = this.props
10    const myStackId = stack.id
11
12    this.setState({
13      selected: selectedStackIds.includes(myStackId.toString())
14    })
15  }
16
17  getButtonClassnames = () => {
18    const { selected } = this.state
19
20    let renderClasses = "btn btn-outline-info btn-sm"
21    if (selected) {
22      renderClasses = "btn btn-outline-info btn-sm active"
23    }
24
25    return renderClasses
26  }
27
28  handleOnClick = event => {
29    let pressed = this.state.selected
30    console.log('button was active: '+ this.state.selected)
31    const stackClicked = event.target.id
32
33    if (!pressed) {
34      this.props.addFilter(stackClicked)
35      this.setState({
36        selected: true
37      })
38    } else {
39      this.props.removeFilter(stackClicked)
40      this.setState({
41        selected: false
42      })
43    }
44  }
45
46  render() {
47    const { stack } = this.props
48    const renderClasses = this.getButtonClassnames()
49
50    return (
51      <button
52        id={stack.id}
53        type="button"
54        className={renderClasses}
55        aria-pressed={this.state.selected}
56        value={stack}
57        onClick={this.handleOnClick}>
58        {stack.name}
59      </button >
60    )
61  }
62 }
63
64 export default FilterButton
Enter fullscreen mode Exit fullscreen mode

When the addFilter or removeFilter function in the ProjectsContainer is invoked, it will execute the action creator below, which will return an action object:

// portfolio-frontend/src/actions/filterProjects.js
    export const addFilter = stackId => {
      return {
        type: 'ADD_FILTER',
        stackId
      }
    }

    export const removeFilter = stackId => {
      return {
        type: 'REMOVE_FILTER',
        stackId
      }
    }
Enter fullscreen mode Exit fullscreen mode

The returned action object will then be dispatched to projectsReducer, which will modify copies of the selectedStackIds and filteredProjects state in the Redux store. The reducer will then return the new version of our global state based on the sent action.

// portfolio-frontend/src/reducers/projectsReducer.js
const projectsReducer = (state = {
  allProjects: [],
  stacks: [],
  selectedStackIds: [],
  filteredProjects: [],
  loading: false,
}, action) => {
  let stackIds
  let filteredProjects = []
...

case 'ADD_FILTER':
      filteredProjects = state.filteredProjects.filter(proj => {
        return proj.stacks.some(stack => stack.id.toString() === action.stackId)
      })

      stackIds = state.selectedStackIds.concat(action.stackId)
      // Set store unique stackIds
      stackIds = [...new Set(stackIds)]

      return {
        ...state,
        selectedStackIds: stackIds,
        filteredProjects: filteredProjects,
      }

    case 'REMOVE_FILTER':
      stackIds = state.selectedStackIds
      stackIds.splice(stackIds.indexOf(action.stackId), 1)

      filteredProjects = state.allProjects
      // only include projects that have all the selected stacks
      if (stackIds.length > 0) {
        filteredProjects = state.allProjects.filter(proj => {
          const projectStacks = proj.stacks.map(proj => proj['id'].toString())
          const includesSelectedStacks = stackIds.every(selectedStack =>
            projectStacks.includes(selectedStack)
          )
          return includesSelectedStacks
        })
      }

      return {
        ...state,
        filteredProjects: filteredProjects,
        selectedStackIds: stackIds,
      }
...
Enter fullscreen mode Exit fullscreen mode

The project components subscribed to Redux store will re-render when state changes, displaying not only the toggled button update but also the filtered project results. This all happens on the client-side without ever needing to communicate with the Rails server.

Filter Projects Demo

Adding Comments to a Project

The addComment action works similarly to the addFilter action. However, instead of just updating the local state, store, and re-rendering the component, it also sends an asynchronous POST request to the Rails API using Javascript’s Fetch API. This is necessary for persisting the new comment record into our Postgres database.

Upon submission of the form, the addComment() function will dispatch the following action to the store:

    // portfolio-frontend/src/actions/addComment.js
    export const addComment = comment => {
      return (dispatch) => {
        fetch(`http://localhost:3000/api/v1/projects/${comment.project_id}/comments`, {
          headers: {
            // data content sent to backend will be json
            'Content-Type': 'application/json',
            // what content types will be accepted on the return of data
            'Accept': 'application/json'
          },
          method: 'POST',
          // tell server to expect data as a JSON string
          body: JSON.stringify(comment)
        })
          //immediately render the new data
          .then(resp => resp.json())
          .then(newComment => dispatch({ type: 'ADD_COMMENT', comment: newComment }))
      }
    }
Enter fullscreen mode Exit fullscreen mode

Here, I am using a middleware Redux Thunk. It allows the action creator to take the dispatch function as an argument, giving us access to dispatch function. Next, we send the action returned by addComment action creator to the projectsReducer immediately after the asynchronous fetch request is resolved.

Lastly, projectsReducer will update our store with the remote data that has just been persisted.

    //portfolio-frontend/src/reducers/projectsReducer.js
    ...
    case 'ADD_COMMENT':
      let index = state.filteredProjects.findIndex(project => project.id === action.comment.project_id)
      let project = state.filteredProjects[index]

      return {
        ...state,
        filteredProjects: [
          ...state.filteredProjects.slice(0, index),
          { ...project, comments: project.comments.concat(action.comment) },
          ...state.filteredProjects.slice(index + 1)
        ]
      }
Enter fullscreen mode Exit fullscreen mode

The new comment component will be rendered in the browser:
New Comment

Conclusion

With this portfolio website, I hope it adds additional color beyond the paper resume. It tells a story of a full stack web developer who can hit the ground running and contribute not only robust code, but also keen design principles.

In addition to what exists now, I also plan on adding a contact page (with a contact form and social media links), a "featured project" button on the homepage to bring the user directly to my latest project showcase, and possibly a dark mode toggle.

I would love to hear your suggestions for any other features that you think might be a great addition to my portfolio. Thank you for reading and stay tuned for the deployed website.

💖 💪 🙅 🚩
jacquelinelam
Jacqueline Lam

Posted on July 15, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related