How to Build Simple React App (Part 4)

How to Build Simple React App (Part 4)

In the previous part we connected our application with RESTful API, which made it more realistic. This part is the final part of our series "How to build simple React app". At the start, we will cover selectors and their usage, and then we will go through styling our application, using .scss.

Filtering todos

The next thing we want to enable in our application is filtering todos so that users can see only finished, unfinished or all todos. This can be done with a simple filter function bypassing the connection between the application state and the component. For example, we can modify our TodoListContainer components mapStateToProps to look like this.


const getVisibleTodos = (visibilityFilter, todos) => {
  switch (visibilityFilter) {
    case FILTER_ALL:
      return todos;
    case FILTER_DONE:
      return todos.filter(todo => todo.done);
    case FILTER_UNDONE:
      return todos.filter(todo => !todo.done);
    default:
      return todos;
  }
}

const mapStateToProps = state => ({
  todos: getVisibleTodos(state.todoReducer.filter, state.todoReducer.todos)
});

This will filter our todos depending on the filter value of our todoReducer. This is a simple and intuitive solution, but it has one problem. It will recalculate to-do list each time when the component is re-rendered. That is where selectors come in. We will use the reselect library for selectors, you can find many examples and explanations about selectors and how they work on their page. Practically what selectors will do is optimize function calls. When we do this through selectors, the function which calculates "visible todos" will be called only when some parts of the state (that the function is using) get changed, and not every time the component is re-rendered. That may be very useful especially when calculations are expensive. Let's see how all this looks like is implemented.

First, we will create a new file for our todo selectors, todoSelectors.js and put it inside our TodoList/reducers/ folder.

// src/components/Home/TodoList/reducers/todoSelectors.js

import { createSelector } from 'reselect';
import { FILTER_ALL, FILTER_DONE, FILTER_UNDONE } from '../constants';

export const getVisibilityFilter = (state) => state.todoReducer.filter;
export const getTodos = (state) => state.todoReducer.todos;

export const getVisibleTodos = createSelector(
  [ getVisibilityFilter, getTodos ],
  (visibilityFilter, todos) => {
    switch (visibilityFilter) {
      case FILTER_ALL:
        return todos;
      case FILTER_DONE:
        return todos.filter(todo => todo.done);
      case FILTER_UNDONE:
        return todos.filter(todo => !todo.done);
      default:
        return todos;
    }
  }
);

The first two functions (getVisibilityFilter and getTodos) are simple selectors (plain functions) which only subtract part of the state relevant to our real selector. getVisibleTodos is the actual selector created with createSelector function (got from reselect library). createSelector will create the function which gets a state as a parameter, then will put that state through all "plain selector functions" we provide as the first argument (in array), and then those extracted values will be passed to the second parameter, which is our filtering function. You see how it works, it creates a wrapper around our "filter" function which decides if the actual function should be called or not. It works similarly to like connect on connecting components with the state (if you remember it won't always send props to the component, but only when relevant parts of the application state change). More on selectors read on their official page.

For this to work you have to install reselect library.

npm install --save reselect

Let's carry on, for now, we are again getting an error about importing a non-existing constants, let's fix that first, we need to add the following three constants in our constants.js.

// src/components/Home/TodoList/constants.js

export const FILTER_ALL = 'ALL';
export const FILTER_DONE = 'DONE';
export const FILTER_UNDONE = 'UNDONE';

Ok, now everything works, but we haven't connected this "selector" anywhere. We will change our TodoListContainer to filter todos before sending them to TodoList. We just need to import our selector and modify our mapStateToProps function a bit.

// src/components/Home/TodoList/TodoListContainer.jsx

...
import { getVisibleTodos } from './reducers/todoSelectors';
...

...
const mapStateToProps = state => ({
  todos: getVisibleTodos(state)
});
...

And of course, we need to add filter property to our global state, otherwise, our getVisibilityFilter (in todoSelectors.js) will always return undefined.

// src/components/Home/Todos/reducers/todoReducer.js

...
const TodoState = new Record({
  todos: [],
  filter: types.FILTER_ALL
});
...

That is it, we now connected everything up. If you change the initial state value of the filter to, for example, types.FILTER_DONE will only see finished todos on screen. That is nice, but we need some kind of public interface to enable users to change the filter. We will do that with the new component.

// src/components/Home/TodoList/FilterSelect.jsx

import React from 'react';
import PropTypes from 'prop-types';

import { FILTER_ALL, FILTER_DONE, FILTER_UNDONE } from './constants';


const handleChange = (e, changeFilter) => changeFilter(e.target.value);

const FilterSelect = ({ changeFilter }) => (
  <select onChange={(e) => handleChange(e, changeFilter)}>
    <option value={FILTER_ALL}>No filter</option>
    <option value={FILTER_DONE}>Show finished only</option>
    <option value={FILTER_UNDONE}>Show unfinished only</option>
  </select>
);

FilterSelect.propTypes = {
  changeFilter: PropTypes.func.isRequired
};

export default FilterSelect;

It is a pretty simple component, just one select with the bound onChange event to a handleChange function which calls changeFilter action (received through props) with the value given from the option tag. Now just render it somewhere on the screen, for example in TodoList after </ul> closing tag. Now we have almost everything connected, but still, in our console, we get an error about failing prop types. Why is that, because our FilterSelect needs changeFilter function passed as a prop, and we are not sending anything. Ok, let's delegate that more. We will modify TodoList to require that function as well and send it down. After that TodoList will look like this.

// src/components/Home/TodoList/TodoList.jsx

import React from 'react';
import PropTypes from 'prop-types';

import Todo from './Todo/Todo';
import AddTodo from './AddTodo/AddTodo';
import FilterSelect from './FilterSelect/FilterSelect';


const TodoList = ({ todos, setTodoDone, deleteTodo, addTodo, changeFilter }) => (
  <div className="todos-holder">
    <h1>Todos go here!</h1>
    <AddTodo addTodo={addTodo} />
    <ul className="todo-list">
      {todos.map((todo) => <Todo key={`TODO#ID_${todo.id}`} todo={todo} setDone={setTodoDone} deleteTodo={deleteTodo} />)}
    </ul>
    <FilterSelect changeFilter={changeFilter} />
  </div>
);

TodoList.propTypes = {
  todos: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    task: PropTypes.string.isRequired,
    done: PropTypes.bool.isRequired
  })).isRequired,
  setTodoDone: PropTypes.func.isRequired,
  deleteTodo: PropTypes.func.isRequired,
  addTodo: PropTypes.func.isRequired,
  changeFilter: PropTypes.func.isRequired
};

export default TodoList;

Now we get two errors, both prop-type errors, one is for TodoList and the other for FilterSelect component, and both for changeFilter function. We need to a create new action and a new reducer handler for this.

// src/components/Home/TodoList/actions/todoActions.js

...
export const changeFilter = (visibilityFilter) => ({
  type: types.CHANGE_FILTER,
  payload: {
    filter: visibilityFilter
  }
});
// src/components/Home/TodoList/reducers/todoReducer.js

// new case added to switch
case types.CHANGE_FILTER:
  return state.set('filter', action.payload.filter);

Don't forget to insert a constant in constants.js

// src/components/Home/TodoList/constants.js

export const CHANGE_FILTER = 'CHANGE_FILTER';

And the last thing, to add this inside our TodoListContainer, just import action from the appropriate action file, and add it inside mapDispatchToProps. And that is all. Now filtering is enabled.

Styling application, and enabling .scss

Each web application needs some style. This part is sometimes done by web designers, but still, sometimes, it is for you to do it, so it is good to know at least the basics of CSS3, .scss and styling HTML. I must state here that I am not a web designer, so this styling isn't done by a professional in that area and probably can be styled better, I just wanted to show you some basics in the styling of the application, but for real application styling you should consult real web designer.

Setup

For styling, we will use .scss format, and to do that we need to make it work with create-react-app because it is not provided by default. There is this great article that writes about adding .scss and .sass into create-react-app and we will do pretty much the same method. We will pick the first method (because it is simpler and more generic), described in detail here.

First of all, we need to add .scss preprocessor (the difference between .sass and .scss is nicely described here), and one more package we will make use of later.

npm install --save node-sass-chokidar npm-run-all

The next thing we need to do is to modify our npm scripts, don't worry if you don't get everything from this part, it is not that important for programming in react, and it is really nicely described on the links I provided up, so you can find it when you need it.

"scripts": {
    "build-css": "node-sass-chokidar src/ -o src/",
    "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
    "start-js": "react-scripts start",
    "start": "npm-run-all -p watch-css start-js",
    "build": "npm run build-css && react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },

What would this do, on npm start it will first run watch-css and then start-js (which is actually our previous start), and watch-css will compile all .scss files into same-name.css files, in the same directory. So from our components, we will still include .css files, even though we haven't created them, or they don't exist at the given moment. That is it, we now can start writing our stylesheets.

Styling

First of all, we will use bootstrap (v4 which was at the time this article was written still in the alpha phase, and here used version is 4.0.0-alpha.6), because it provides a lot of things already implemented, so we can use it (with some modifications) to get it up and running fast. To do that, we will modify the base HTML template used for our application public/index.html. We need to add a stylesheet CDN link in the head tag (on the end) and script CDN links to the end of the body tag.

<!-- Bootstrap stylesheet link, end of the <head> -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">


<!-- Bootstrap scripts, end of the <body> tag -->
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>

And that is it, we have included bootstrap in our app, so we can use it freely inside every component. The next thing we want to do is to override current css files into scss. Let's start from the top down. First, we will create one file just for constants. we will put it inside src/components/common/styles/variables.scss.

/* src/components/common/styles/variables.scss */

$background-lighter: #3a3a3a;
$background-darker: #222222;
$white: #FFFFFF;
$black: #000000;
$white-shadowed: #C9C9C9;

That defines all of colors we will use through the application, in all other stylesheet files we will include this file, and use those variables. Next is Root.

/* src/components/Root/assets/styles/index.scss */

@import '../../../common/styles/variables.scss';

body {
  margin: 0;
  padding: 0;
  font-family: sans-serif;
  background-color: $background-lighter;
}

.dark-input {
  background-color: $background-lighter !important;
  color: $white !important;


  &::-webkit-input-placeholder {
    color: $white-shadowed !important;
  }
  
  &:-moz-placeholder { /* Firefox 18- */
    color: $white-shadowed !important;  
  }
 
  &::-moz-placeholder {  /* Firefox 19+ */
    color: $white-shadowed !important;  
  }
 
  &:-ms-input-placeholder {  
    color: $white-shadowed !important;  
  }
}

.dark-select {
  background-color: $background-lighter !important;
  color: $white !important;

  option {
    color: $white !important;
  }
}

We defined a very simple style for body tag, we used the $background-lighter variable to define the body background color. And we defined two global classes, .dark-input and .dark-select, which will be used somewhere later, they just provide styles for input and select tags, accordingly. Just make sure that src/components/Root/Root.jsx includes ./assets/styles/index.css. Note again that components are still importing .css files and not .scss even though we are writing .scss.

Next is NotFound, we renamed not-found.css into the index.scss, and that is it, its content stays the same, the only thing that changed is the name, so we need to fix import inside NotFound.jsx

// from
import './assets/styles/not-found.css';

// to
import './assets/styles/index.css';

And we got to Home, where we will actually make some changes. First of all, we rename our Home/assets/styles/home.css into Home/assets/styles/index.scss and replace the content with

/* src/components/Home/assets/styles/index.scss */

@import '../../../common/styles/variables.scss';

.app-header {
  background-color: $background-darker;
  height: 72px;
  padding: 20px;
  color: white;
  text-align: center;
}

.main-content {
  width: 70%;

  margin: 2% auto;
  padding: 5% 10%;
  border-radius: 33px;
  background-color: $background-darker;
  color: $white;

  -webkit-box-shadow: 10px 10px 26px 0px rgba(0,0,0,0.75);
  -moz-box-shadow: 10px 10px 26px 0px rgba(0,0,0,0.75);
  box-shadow: 10px 10px 26px 0px rgba(0,0,0,0.75);
}

And accordingly, change html structure

// rendering html in src/components/Home/Home.jsx

<div>
  <div className="app-header">
    <h2>ToDo App</h2>
  </div>
  <div className="main-content">
    <TodoList />
  </div>
</div>

We extracted some stuff we don't need anymore, it is simplified, and more compact now. One note, for box-shadow property there is a site, which generates code for it, pretty cool tool, you can find it here. Now we go into styling TodoList. Same as before we create assets/styles/index.scss file and import it inside TodoList component. Style content is again pretty simple.

@import '../../../../common/styles/variables.scss';

.todo-list {
  margin: 30px 0;
  list-style-type: none;

  border: 1px dashed;
  padding: 30px;
}

And rendering html, is pretty similar.

// rendering html of `src/components/Home/TodoList/TodoList.jsx

<div>
  <AddTodo addTodo={addTodo} />
  <ul className="todo-list">
    {todos.map((todo) => <Todo key={`TODO#ID_${todo.id}`} todo={todo} setDone={setTodoDone} deleteTodo={deleteTodo} />)}
  </ul>
  <FilterSelect changeFilter={changeFilter} />
</div>

Three more components to go. Let's start with AddTodo. Here we don't need any special style defined, so we don't define assets/style/index.scss (but that would you do in a moment when you need some style for that component), we just change the html a bit.

// rendering html of `src/compoennts/Home/TodoList/AddTodo/AddTodo.jsx

<div className="form-group row">
  <input 
    className="form-control dark-input"
    type="text"
    onChange={this.changeTaskText}
    onKeyPress={this.handleKeyPress}
    value={this.state.task}
    placeholder="Task text"
  />
  {this.state.task ? <small class="form-text">Press enter to submit todo</small> : null}
</div>

Have you noticed that there is no submit button any more? We changed that, for styling purposes, it looks better with input only, but how do we now submit it? In <input> tag we added onKeyPress handler, mapped to a function this.handleKyePress, so let's see that function.

class AddTodo extends Component {
  ...
  constructor(props) {
    ...
    this.handleKeyPress = this.handleKeyPress.bind(this);
  }
  
  ...
  handleKeyPress(e) {
    if (e.key === 'Enter')
      this.submitTask(e);
  }
  
  ...
}
...

Straightforward function, that just checks if the pressed key was enter, and if it is, it calls submitTask function, which, if you remember, was our handler for the submit button. Because this can be a little confusing for a user, we added a little note below the input field, which shows only if the input field contains text, and guides the user on how to submit todo. Also, note that here we are using that class we defined inside Root/assets/styles/index.scss, .dark-input, which was extracted to root because it isn't something bound for AddTodo component, it is just a look of an input field, we may need it somewhere else in the project, not only here, that is why those classes are extracted. Ok, next is Todo, there we need some style.

/* src/components/Home/TodoList/Todo/assets/styles/index.scss */

@import '../../../../../common/styles/variables.scss';

.todo-holder {
  display: flex;
  flex-direction: row;

  margin: 10px 0;

  border: 1px dashed;
  padding: 15px;

  &.done {
    background-color: $background-lighter;

    .text {
      text-decoration: line-through;
    }
  }

  .text {
    flex: 7;
    text-align: left;
    margin: 0;

    /* Center text verticaly */
    display: flex;
    align-items: center;
  }

  .buttons {
    flex: 3;

    delete-button {
      border: none;
      padding: 0;

      cursor: pointer;
    }

    .done-button {
      border: none;
      padding: 0;

      cursor: pointer;      
    }

    .control-image {
      width: 24px;
    }
  }
}

Nothing complicated, let's see html changes

// rendering html of src/components/Home/TodoList/Todo/Todo.jsx

<li className={'todo-holder ' + (todo.done ? 'done' : '')}>
  <p className="text">{todo.task}</p>
  <div className="buttons">
    <a className="done-button" onClick={(e) => { e.preventDefault(); setDone(todo, !todo.done) }}>
      {
        todo.done ? 
          <img src={reactivateImg} className="control-image" alt="Reactivate" /> :
          <img src={doneImg} className="control-image" alt="Set Done" />
      }
    </a>&nbsp;
    <a className="delete-button" onClick={(e) => { e.preventDefault(); deleteTodo(todo.id) }}>
      <img src={deleteImg} className="control-image" alt="Delete" />
    </a>
  </div>
</li>

First of all, we added todo-holder class to each <li> element and removed that inlined style for done tasks into a class. Task text is wrapped inside text class, and buttons inside buttons class, buttons are changed from <button> tag into <a> tags with images inside, and in onClick handlers are added e.preventDefault(); on beginning so that the link doesn't actually go somewhere (top of the page). And last but not least FilterSelect. We haven't added any special styles here either. But html changed a bit.

// rendering html of src/components/Home/TodoList/FilterSelect/FilterSelect.jsx

<div className="form-group row">
  <select className="form-control dark-select" onChange={(e) => handleChange(e, changeFilter)}>
    <option value={FILTER_ALL}>No filter</option>
    <option value={FILTER_DONE}>Show finished only</option>
    <option value={FILTER_UNDONE}>Show unfinished only</option>
  </select>
</div>

Nothing special we added some bootstrap classes, and .dark-select from our global stylesheet (Root/assets/styles/index.scss). And that is it!

Conclusion

With this part, we have finished this series about building react the application from ground up. We have covered most of the main parts you would need while building a real react application. Some parts are covered in more depth than others, but that doesn't necessarily mean that they are more important. I encourage you to read through the documentation of all libraries that you are using and to read more articles written on this topic while working, it is very useful, and that is why I have linked many things I found useful in the text(s). You can find all of the source code on the GitHub link. That is it, I hope this was helpful.