DEV Community

Nesha Zoric
Nesha Zoric

Posted on

Building a 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". On start, we will cover selectors and their usage, and then we will go through styling our application, using .scss.

Filtering todos

Next thing we want to enable in our application is filtering todos so that user can see only finished, unfinished or all todos. This can be done with simple filter function bypassing connection between application state and 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 filter value of our todoReducer. This is asimple and intuitive solution, but it has one problem. It will recalculate todo list each time when the component is re-rendered. That is where selectors come in. We will use 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, function which calculates "visible todos" will be called only when some parts of the state (that function is using) gets changed, and not every time component is re-rendered. That may be very useful especially when calculations are expensive. Let's see how does all this look like 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;
    }
  }
);

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 actual selector created with createSelector function (got from reselect library). createSelector will create thr function which gets a state as a parameter, then will put that state through all "plain selector functions" we provide as first argument (in array), and then those extracted values will be passed to the second parameter, which is our filtering function. You see how does it work, it creates a wrapper around our "filter" function which decides if the actual function should be called or not. It works similar 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 application state changes). More on selectors read on their official page.

In order 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 non-existing constant, let's fix that first, we need to add 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 to 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 initial state value of 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 thr binded onChange event to a handleChange function which calls changeFilter action (received through props) with thr value given from option tag. Now just render it somewhere on thr 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 to 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 other for FilterSelect component, and both for changeFilter function. We need to a create new action and 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 constant in constants.js

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

export const CHANGE_FILTER = 'CHANGE_FILTER';

And thr last thing, to add this inside our TodoListContainer, just import action from 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 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 professional in that area, and probably can be styled better, I just wanted to show you some basics in the styling of 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 are nicely described here), and one more package we will make use of later.

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

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 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 same directory. So from our components we will still include .css files, even though we haven't created them, or they don't exist in given moment. That is it, we now can start writing our stylesheets.

Styling

First of all, we will use bootstrap (v4 which is in the time this article is written still in 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 base HTML template used for our application public/index.html. We need to add 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 into our app, so we can use it freely inside every component. Next thing we want to do is to override current css files into scss. Let's start from 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 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 very simple style for body tag, we used $background-lighter variable to define body background color. And we defined two global classes, .dark-input and .dark-select, which will use 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, 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, here 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 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, 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 from 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 moment when you need some style for that component), we just change a 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? 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, just checks if the pressed key was enter, and if it is, it calls submitTask function, which, if you remember, was our handler for 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 input field contains text, and guides user how to submit todo. Also, note that here we are using that class we defined inside Root/assets/styles/index.scss, .dark-input, that was extracted to root, because it isn't something bound for AddTodo component, it is just a look of a 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 al, 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 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 application from grounds 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 then others, that doesn't necessarily mean that they are more important. I encourage you to read through documentation of all libraries that you are using, and to read more articles written on this topic while working, it is very useful, that is why I have linked many things I found useful in the text(s). You can find all of the source code on GitHub link. That is it, I hope this was helpful.

Originally published on Kolosek blog.

Top comments (0)