To declare a variable, you can either use const
(for constants), or let
(for variables that can change over time).
tl;dr: Do not use var
.
var
is a variable that can have multiple values like let
, but it's visible in the outer scope (which means, globally if declared outside a function, in the whole function body if declared inside). let
will only be visible inside the scope it's declared in (e.g., a for
loop inside a function).
const count = 10;
for (let i = 0; i < count; i++) {
console.log(`Hello ${count}`);
}
You can use single, double, and inverse quotes to create string literals, only backticks can interpolate variables though.
const name = "Daniele";
console.log("Hello " + name);
console.log('Hello ' + name);
console.log(`Hello ${name}`);
To create arrays, you can use square brackets []
.
const anArray = [1, 2, 3];
To create objects (string indexed dictionaries), you can use curly brackets.
const obj = {
a: 1,
b: "Hello",
c: {
nested: true
}
};
To create a function, you can use both the function
keyword, or use the arrow =>
syntax. Arrow functions can directly return a value as an expression, and don't need ()
around a single argument. They need ()
around the return value if they return an object.
tl;dr: they're mostly interchangeable, but prefer arrow syntax over function.
There are 2 main differences between arrow and function syntax:
- Arrow functions cannot be used as constructors (a function works as a class constructor of itself, kind of,
class
is just syntactical sugar). - The
this
keyword: in JS, it represents the context with which the function is executed. In functions, the context might change depending on how the function is invoked (for example, you could provide a differentthis
usingFunction::apply(thisArg, args)
). In arrow functions, it doesn't change at runtime (it will inherit the parent context, in the current scope).
function say(name) {
console.log(`Hello ${name}`);
};
const say = (name) => {
console.log(`Hello ${name}`);
};
const double = n => n * 2;
const makeConfig = () => ({debug: true, log: false}); // returns the object
You can spread objects and arrays, which applies their content to the current expression. The order in which you spread your object matters. In case of objects with the same fields, earlier declarations are overridden by the matching ones that come later.
const numbers = [1, 2, 3];
const add = (a, b, c) => a + b + c;
const total = add(...numbers); // 6
const twice = [...numbers, ...numbers]; // [1,2,3,1,2,3]
const defaults = {
log: false,
debug: true
}
const options = {
debug: false
}
const overrideOptions = {...options, ...defaults} // {log: false, debug: true}
const overrideDefaults = {...defaults, ...options} // {log: false, debug: false}
const overrideWithFields = {...defaults, debug: false} // {log: false, debug: false}
You can destructure (access their inner values directly) objects and arrays.
const [first, second] = [1, 2, 3]; // first: 1, second: 2
const [, last] = [1, 2, 3]; // last: 3
const add = ([first, second]) => first + second;
add([1, 2]);
const {a, b} = {a: 1, b: 2}; // a: 1, b: 2
const add = ({first, second}) => first + second;
add({first: 1, second: 2});
To check for equality, always use the triple equal ===
. Objects are never equal between each other. ==
casts the two object to the same type before comparing, and assumptions on the behaviour might lead to unexpected results because the implementation might be not intuitive.
const a = 1;
const b = "1";
const c = 1;
a === b // false
a === c // true
{a: 1} === {a: 1} // false
To declare a class, use the class
keyword. To reference fields, use the this
keyword.
class Hello extends World {
constructor(options) {
super(options);
this.name = options.name;
}
speak() { // method declared as a function
console.log(`Hi ${this.name}`);
}
fullName = () => this.name + this.surname; // method declared as arrow function
}
You can use export
to export a variable/class/function. One of the exports can be declared as a default
export.
You can use import
to import other JS modules: you can access local files using a relative path, and dependencies installed via NPM using the name of the package in the import statement.
To import a default export:
// hello.js
// you can export a class as default in one line
export default class Hello {};
// or, if you're exporting a variable
const Hello = "world";
export default Hello;
// anotherFile.js
import React from "react"; // importing from a dependency
import Hello from "./hello"; // importing from a local file
To import other exports:
// hello.js
// to export a class as non default, you need to export it after being declared
class Hello {};
export {
Hello
}
// you can export a variable in one line
export const Hello = "world";
// anotherFile.js
import {Component} from "react";
import {Hello} from "./hello";
// or alternatively, you can have one import and reference fields
import AnyName from "hello";
doSomething(AnyName.Hello)
- Clone the kata
- Install the dependencies with
yarn install
- Start your app with
yarn ios
oryarn android
, remember that you'll need an Android emulator already running for it to work! cmd+M
/cmd+D
(Android/iOS) opens the dev menu, try to make some changes and reload, try the auto reload!R,R
/cmd+R
reloads the page.- Attach the debugger and try to set a breakpoint in Chrome.
Currently, App.js
is quite boring, it would be great if instead of having the TODOs hardcoded, we could somehow cycle through an array of TODOs and show a Text
per element.
First of all, we need to extract the data for the TODOs, in App.js
:
const todos = [{
name: "Take the dog out",
}, // ...
];
The render
method must return a hierarchy of views that we want to display. We can map
our todos to views inside that method.
const todoElements = todos.map(todo => <Text>{todo.name}</Text>);
Inside a component you can include a JS expression with {expression}
, in this case, {todo.name}
.
Since todoElements
is an expression, we can include it inside our View
, instead of the 3 Text
elements. Remember to use {}
!
If you reloaded, you'll notice there's a small yellow warning on the bottom of the app. Components rendered with loops require a key
property that identifies them.
Keys are important for React to be able to understand which views have been added, removed or altered.
As you can do inside a component, you can use propName={expression}
to pass properties. If it's a hardcoded string, prefer propName="value"
.
Before moving to the next step, let's use todo.name
as a key for our elements.
Now that our single TODO is well isolated, we can extract it to a separate component.
The simplest component (a stateless component) is a function that returns a tree of elements. It must have no side effects and return the same result given the same input.
const Hello = ({name}) => (
<Text>Hello {name}!</Text>
);
The first argument of the function is the properties passed to the component. We're destructuring it and accessing name
directly.
You can then import your new component and use it as any other React component. If you declared the component in another file, make sure to export it! To pass props to a component, add them as an attribute when you use it.
<Hello name="Daniele" />
Note that, in this case, since we don't accept children (components inside our component), the component has only one tag, which is called self-closing, and terminates with />
instead of just >
.
Extract a TodoItem
component, that accepts a name
parameter, and substitute the Text
s with it.
Our TODO list is coming together quite well, but it won't scroll!
There are a few possible solutions to it:
- Using
ScrollView
instead of ourView
element would make it scrollable. - Using
FlatList
, a similar concept toRecyclerView
on Android andUICollectionView
on iOS.
ScrollView
is usually picked when we have a fixed layout that might not fit in the container, FlatList
is used when we have an arbitrary amount of data and we want to render only the components that are visible on the screen. We will go with FlatList
.
FlatList
accepts various properties, we will focus on 2 of them:
data
, which represents the data we want to render.renderItem
, which is expected to be a function. It receives an object containing 2 keys:item
andindex
, representing the current item in the list and its index, and must return a tree of elements.
We could reuse the same function we used with todos.map()
earlier, as long as we change how we access the TODOs (the parameters are inside an object, you can destructure to access them directly).
Extract a TodoList
element, that accepts a property todos
, a list of TODOs, and uses FlatList
to render TodoItem
s.
You should now have another warning: FlatList
needs to somehow figure out the object key. There are 2 ways of solving this.
- Add a unique
key
field to each one of our TODOs (the data).
const todos = [{
name: "Take the dog out",
key: "abc"
}];
- Alternatively, we can pass an additional property to
TodoList
:keyExtractor
. This is a function that receives two parameters:item
andindex
(not inside an object this time), and needs to return a string representing the key of this element.
Pick one of the two options and get rid of the warning, then remove the property key
from the TodoItem
element.
It's time to tick some TODOs. let's add a new field to them: completed
, and set it to false
.
const todos = [{
name: "Take the dog out",
completed: false
}];
Now, let's add that to our properties in Todo
:
const Todo = ({name, completed}) => ...
To display the two different states of a TODO (completed, incomplete), we can use a Switch
, which accepts a property named value
.
If you tried to click one of the switches, you will have noticed that the switch goes back in its original position. This happens because Switch
is a (controlled component)[https://reactjs.org/docs/forms.html#controlled-components], which in short means that its state is only updated when invoking setState()
.
Usually, React applications tend to have components that hold no state, and which are only responsible for displaying the current state of the app via properties. The state is then usually kept outside of them. In our case, it means we can hold the state inside the App
component.
As your app grows, keeping the state inside App
might become cumbersome. In that case, you might want to look at other solutions, for example, Redux.
The first thing we need to do is to move the TODOs inside the state of component and use it:
const todos = [...];
class App extends Component {
state = {
todos: todos
}
render() {
return <TodoList todos={this.state.todos} />
}
}
This will have the result to set the initial state and use it to render our list.
After this, we need to react to inputs by the user. To do that, we need to add a callback to the Switch
components, and propagate the event up to the App
component. A callback is just another property in the component, which expects a function as its value. In the case of Switch
, the property name is onValueChanged
, which receives a boolean representing the new value (true
/false
).
If we add a callback to Todo
, TodoList
can then react to the event. If we do that also in TodoList
, we can intercept the event in the App
component.
To alter the state of a component, you need to invoke this.setState
inside that component. Writing directly to this.state
will not ensure the propagation of the change. setState
can be invoked in two ways:
setState({key: newValue})
, which alterskey
to represent our new state. This merges the previous state with the keys provided, so if you alter only one of 2 keys in the state, the second one will remain untouched. Note that this is a shallow approach, and works only on the keys in the root.setState(oldState => newState)
, which accepts a function.
The second case is required when your update depends on the current state. The function receives the current state and must return the new state (which is then merged, like the previous case). Since state updates are asynchronous, this is necessary because you're not ensured to have the latest state inside this.state
.
Add a callback to the various components, and update the state inside App
.
The standard interface to do asynchronous work in JS is Promise
. A promise is an object representing something (for example, a network request) that will end in the future.
Unlike Observable
, a Promise
is immediately executed upon creation!
To access the results of a promise (the promise was resolved), you need to invoke the then
method and provide a callback to it. The callback will receive the result of the operation as the only parameter.
If you return a value inside that callback, you'll create a new promise which will resolve to that return value. You can also return a promise inside a promise, it will be resolved before calling the next step.
Promise.resolve(2) // creates a promise that immediately resolves with value 2
.then(n => n * 2) // n = 2
.then(m => Promise.resolve(m * 3)) // m = 4
.then(o => doSomething(o)) // o = 12
To intercept eventual errors in a promise (the promise was rejected), you need to invoke the catch
method and provide a callback to it. The callback will receive the error as the only parameter.
If you throw inside a then
(or return a rejected promise, for example Promise.reject
), the promise will be rejected and you can catch
after it.
If you return a value in catch
, it creates a new promise resolving to that value.
Promise.resolve("Hello!")
.then(n => { throw new Error("My mistake!") })
.catch(e => "No problem");
.then(value => Promise.reject("I don't like this"))
If you don't catch
a rejected promise, it will throw an error and crash (the same as if you didn't catch an exception in a try/catch statement).
Let's make sure that changes in our list are persisted between apps restarts. To do that, we will be using AsyncStorage
.
To retrieve something stored, we can use getItem
: it accepts a key and it returns a promise, the result will be either the previously stored data, or null
if there isn't any.
To retrieve the state when the application is initialized, we can use one of the lifecycle callbacks of a component (they're present only in class components), componentDidMount
and set the retrieved state inside the promise callback.
componentDidMount() {
AsyncStorage.get('todos')
.then(todos => this.setState({todos: todos}));
}
To store something, we can use setItem
: it accepts a key and the value that we want to store, it returns a promise that will be resolved when the data is stored (in this case, when the promise is resolved, we won't receive any value).
There isn't a standard lifecycle method that we can use to persist our state. componentWillUnmount
will be invoked when the application is shutting down, but it won't wait for AsyncStorage
to write to disk. One possible option is to save the state every time we update it.
const onTodoChanged = (todo, index) => {
this.setState({todos: newTodos});
AsyncStorage.set('todos', newTodos);
}
Try to toggle some todos and restart the app, the changes should now be kept!
We need a way to add a TODO after the app started. We could navigate to a new page, add the TODO and go back, but for the sake of this exercise we'll use a (Modal
)[https://facebook.github.io/react-native/docs/modal] instead.
A Modal
is a component that will be drawn over other widgets, and has 2 states: visible and invisible. Differently from native world, the modal component is always present in the tree, and controlling its visibility is done via the visible
boolean property.
Note that having a component in the tree in React doesn't necessarily mean that component will be present in the view hierarchy. React only generates a tree representing the intended view hierarchy, the native driver implementation that makes use of that data will then decide to add or remove that view from the hierarchy (the decision is based on the difference between the previous and the current tree).
Inside a modal, you can have any view, remember to use SafeAreaView
at the root to avoid the content going under system areas in iOS!
To react to the user wanting to close the modal (tapping the back button, or tapping outside), we can add the onRequestClose
property to it, which is a callback invoked when that happens.
You can also close programmatically the modal, just by altering the state and setting the visible
property to false
. You will have to do that also when intercepting onRequestClose
.
We need to create a component to add TODOs, and we will need a few things from it:
- It needs to hold the modal visibility, so it'll have to be a class component holding state.
- It needs to expose a callback
onTodoSaved(todo)
to intercept when the user completes adding a TODO.
To do that, AddTodo
should hold 2 components: a Button
to trigger the modal opening, and the modal itself. The modal should contain a TextInput
for the user to write the TODO name in (remember, it's a controlled component, like Switch
), and a Button
to save the TODO.