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