Building My Personal Portfolio with React & Redux (pt.2)
Tags: react, redux, rails, javascript
Published: 2020-07-15 13:08:59

In [part 1 of my article](https://dev.to/jacquelinelam/building-my-personal-portfolio-with-react-redux-pt-1-522k), 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 <a href="https://www.tutorialspoint.com/redux/redux_data_flow.htm" target="_blank">Redux Flow</a>.

### Feature Highlights

#### Filtering Projects By Stacks
Some research show that <a href="https://www.hrdive.com/news/eye-tracking-study-shows-recruiters-look-at-resumes-for-7-seconds/541582/#:~:text=Dive%20Brief%3A,an%20average%20of%207.4%20seconds.">“6 seconds is the average time recruiters spent reading a resume”</a>. 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.)

```jsx
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
```


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

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

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.

```jsx
// 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,
      }
...
```
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](https://dev-to-uploads.s3.amazonaws.com/i/pyoprxy9pv5a2ny8oh7k.gif)

 
#### 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:
```jsx
    // 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 }))
      }
    }
```

Here, I am using a middleware **[Redux Thunk](https://daveceddia.com/what-is-a-thunk/#:~:text=thunk%2C%20n.,function%20that's%20returned%20by%20another.)**. 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.
```jsx
    //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)
        ]
      }
```
The new `comment` component will be rendered in the browser:
![New Comment](https://dev-to-uploads.s3.amazonaws.com/i/6pdoqf6a6typ90ykpotv.gif)

### 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.