Photo by Andras Vas on Unsplash
As a Javascript dev, React has been an integral part of my learning experience. I started learning and writing Javascript in October of 2017, primarily in the context of Rails. Needless to say, there was a lot of jQuery involved and the experience was not particularly pleasant. My work with React actually started in React Native, but this article will strictly be for React web. Most of the concepts are applicable, but the presentation semantics will change in Native (the HTML elements must be switched for React Native <View>s <Text>s, <FlatList>, etc.)
Today we will be building a Todo List. This article is meant to illustrate the setup of a React app with Babel/Webpack and Flow for type checking.
The finished repo for part one is here: React Todo
In this article we will primarily focus on using Flow types for Components. Some knowledge of React is assumed.
Disclaimer: I will not be going in-depth into configuring webpack. I mostly use a boilerplate configuration from React-Redux Boilerplate, which is a fantastic starting point for production-ready React apps, but not intended for beginners. If you are interested in learning more about Webpack configuration, here is a great resource: How to Develop React.js Apps Fast Using Webpack 4
This article will serve as an introduction to Flow types and static type-checking more generally. It is assumed you have some familiarity with React, but a great deal is not necessary.
To start coding right away, run this command in your terminal:
Some explanation behind the base configuration:
For this tutorial I am going to take some steps so we can use webpack hot reloading in development. First I copied over my usual webpack config/
and server/
directories, then I copy over the src/
folder from a create-react-app
application and add a small index.html
. (All of these are found in the repo)
I keep a private repo with my preferred base configuration (I didn’t use it here because it includes extras like Redux Saga and reselect that we will be adding in Part 2 of this series) that I clone to bootstrap a new app with these defaults.
Next, I set up my package.json
as follows:
I also copy over my personal .eslintrc and the following .babelrc:
To begin development, create a .flowconfig and then start the flow server
Put the following into your .flowconfig to stop flow from type-checking node_modules:
Now start the server using:
$ yarn start
and go to http://localhost:3000 in your browser.
Next, let’s build our first Flow typed component.
So our todo list requires some essential functionality:
So where should we begin? Since we are not using redux (yet), we need one of our components to have and keep track of state. I think in this case we are best served tracking state in the root App. So lets change App.js to this:
For now, we’ve just declared our root component: React Apps always load using a “Root” component, generally named App
. A Flow-typed component carries the following class signature: class ComponentName extends Component<Props, State>
Our root component is not going to receive any Props, and the State type declaration looks like this:
type AppState = {| todos: Array<any> |}
For now, we are using any
because we haven't really determined what a Todo will be yet. We're using the pipes {| |}
to tell Flow this is the exact shape of AppState - it should not contain any other keys or be missing the todos
key.
Also notice the initialization of default state as a property rather than in the constructor.
Let’s go ahead and define the behaviors for updating our App state here as well:
const flipCompleted: (item: any) => * = item => ({...item,completed: !item.completed,})
class App extends PureComponent<any, AppState> {state: AppState = {todos: [],}
addTodo = (todo: any): void => this.setState(state => ({todos: state.todos.concat(todo),}))
removeTodo = (todoID: number): void => this.setState(state => ({todos: state.todos.filter((todo: any): boolean => todo.id !== todoID)}))
completeTodo = (todo: any): void => {const updateTodos = existing => (existing.id === todo.id? flipCompleted(todo): existing)const newTodosState: (state: AppState) => AppState = state => ({todos: state.todos.map(updateTodos)})
this.setState(newTodosState)
}
render() {const { todos } = this.state
return (
<div className="App">
<span className="status-text">
Total: {todos.length}
</span>
</div>
)
}}
These are a little weird looking, so I’ll explain each one:
addTodo
:
Our addTodo function just accepts a todo item and updates state to include it. the type signature (todo: any): void
lets us know we shouldn't expect a return value when we call this.addTodo(todo)
.
removeTodo
:
Remove todo is going to accept a todo ID and iterate over the array of todos to remove the matching ID. Not the most performant solution, but we aren’t building for speed yet.
completeTodo
:
Complete todo is going to accept an entire todo item and create a new todo item with its completed
property flipped from true => false and vice versa (completed: !todo.completed) and then replace the item in the list of todos.
A note on the type signature:
type UpdateFunction = (item: any) => *
A *
is what’s known in Flow as the Existential Type
. This is a placeholder type. It is similar to any
, but allows you to maintain a certain amount of type-safety, by telling the type checker to infer the returned type, as opposed to any
which Flow will ignore altogether. A related concept is Generic
types, which we will go over later.
Our removeTodo and completeTodo functions have some issues. Both removeTodo
and completeTodo
have dependencies on the Todo item having an id
property and in the case of completeTodo
a completed
property. Instead of defining the TodoType here, let's go ahead and start building our TodoList
component.
Let’s make a directory in src/components/
:
Let’s start with a single Todo and then work our way up to the logic we need for the list.
In here we’ve finally defined our TodoType
:
Notice we’re again using pipes to tell Flow this is the exact shape of a TodoType. Here we also define our presentation logic for Todos, like connecting the ability to remove one to a button, or associating a checkbox with the completed
property and having it handle this.onCompleteTodo
:
As far as onCompleteTodo
, we're going to call the function passed in by props with the todo itself as the argument. To make sure the function passed into props has the right signature, we defined our TodoItem
's prop types:
Here we declare that we want our TodoItem
component to receive 3 props, and none of them can be undefined
:
TodoType
shapeTodoType
object as an argument, with a void return typeTo clarify the onRemove
function we'll look at the implementation of our TodoList
:
Ok, let’s go through the props our List wants to receive first:
Looks familiar, right? the props here correspond directly with the state/behavior we defined in our Root component. The component that maintains state is generally referred to as a container
component. Since we only have one in this app i haven't bothered, but generally good practice is to separate stateful containers
and presentational components
into different directories. In any case, this component is going to receive the list of todos and the behaviors associated with manipulating it as props.
import type { TodoType } from './TodoItem'
We import
our TodoType
(using import type
so the dependency is stripped by babel, in cases where the only dependency is on type definitions)
This function probably makes more sense in the form component itself, but it could be argued that the logic of collecting the data that corresponds to an item shouldn’t be coupled to the shape of the item or the way you persist it. In any case, this accepts some text as a todo item, assigns it an ID (the current date as milliseconds since epoch) and gives it the default state of completed: false
. This function gets called here:
A few notes: compose
: a function that accepts any number of (unary) functions, and returns a function that is the result of composing the function returns from right to left. so in this case,
It may look a little strange, but composition is a very powerful concept. the shorthand
works because in javascript functions are first class. Read more about functional programming in javascript in this excellent series: Composing Software: An Introduction
onRemoveTodo
also has some functional flavor, courtesy of currying:
Remember that onRemove
prop we referenced in the TodoItem
? Well it doesn't need any arguments because the event handler we passed in was a curried function that we defined here. when we render a todo item we pass this function as this.onRemoveTodo(todo.id)
and the return value (that the component receives as a prop) is a function with this signature () => void
. Because the function retains lexical scope, the todoID
we pass in the definition here will be the value this.props.onRemoveTodo(todoID)
receives.
This means our renderList function looks like this:
We take each todo in the Array<TodoType>
, and create a Todo
with the required props.
the reason I used a curried function for onRemove is so the onClick handler in checkbox could be defined point-free (onClick={this.props.onRemove}
). This same strategy can be used to refactor our TodoItem into a stateless functional component, by currying onCompleted:
Finally, our render function simply invokes the renderTodos function as well as placing our NewTodoForm
above the list for easy access.
Component props/state:
This is what’s known as a controlled input, meaning the state of the input field is controlled by our application, rather than accepting the user’s input and directly assigning it to the value
attribute of the input element in the DOM. This allows you to intercept the value, reformat it, validate, or do whatever you want. In our case we aren't doing anything but saving it in state. This allows us to easily clear the input when the user submits by setting the state field to an empty string:
Because we de-coupled our todo item logic from our form, our form isn’t responsible for building the todo. it simply calls the onSubmit function with the text from the form. If you wanted to add some validation for the form, you would have the option of doing so in the passed in onSubmit
. It isn't perfect, but it gets the job done here (if you wanted to validate this form, it'd probably be best to use a validate
function as a prop that the form could call before deciding to call onSubmit
and clear its state).
Here we are just exposing our TodoList as the default export and defining a type export for TodoType (so our App component can use it).
Now our TodoList is assembled, it’s time to use it in our App
component. We can also update our type signatures:
On our way out I decided to spice things up with a custom count function. It simply accepts an array of generic objects and a predicate function, and returns the number of elements for which the function returns true.
This lets us display the count of the completed items as well as the total.
Some notes on the Flow types used here:
type Predicate<T> = (item: T) => boolean
In this type declaration, T is a generic type. When we define our count function, we use another generic type this way:
const count = <K>(list: Array<K>, predicate: Predicate<K>): number => list.reduce((acc, item: K): number => (predicate(item) ? acc + 1 : acc),0,)
So this is saying:
A Predicate<T> is a function that accepts an (item: T) and returns a booleancount<K> is a function that accepts (list: Array<K>, predicate: Predicate<K>), and returns a number
Generic types are a way to describe function behavior in static type checking systems. It uses what’s called a type argument to indicate the actual type will be provided on invocation. Flow can use this definition and understand that if count is called with an Array of K, the Predicate function needs to accept an item of type K as an argument, and return a boolean.
This helps Flow to understand what you want your code to do, so later on if I try this:
// in some other filetype Stuff = { item: string }const arrayOfStuff: Array<Stuff> = [{ item: 'hey' }, { item: 'hi' }]export default arrayOfStuff
// in yet another fileimport array from './someOtherFile'import count from './App.js'
count(array, thing => !!thing.notItem)// cannot get thing.notItem because property notItem is missing in 'Stuff'
Flow will let me know this code I’m trying to type makes no sense.
Otherwise here we are simply adding the call to our new TodoList component and passing in the props it expects.
I hope I have illustrated some of the benefits of a static type-checking system for development. While this is hardly a comprehensive guide, it should serve to introduce some of the basic and important concepts of static types, and (hopefully) provide reason to use a type-checker for your JS projects.