- download node and npm
- install the react boiler plate CLI
npm i -g create-react-app
- create your add
create-react-app <app-name>
- run the app for the first time.
cd <app-name> yarn start
- in order to make the ui more friendly and neat i will use a third party css package call
bootstrap
- add
bootstrap
to your application by run:yarn add bootstrap
- on the
App.js
add thebootstrap
css:import 'bootstrap/dist/css/bootstrap.min.css';
- under
src
create new folder,users
. on that folder create new foldercomponents
(src/users/components
). - in this folder create
User.js
file:
/* src/users/components/User.js */
import React, { Component } from 'react'
export default class User extends React.Component {
render() {
return (
<div className="item">
<div>
id:1
</div>
<div>
username:johndoe
</div>
<div>
email:[email protected]
</div>
</div>
);
}
}
- inside
User.js
create aconstructor
function. paste thereconsole.log('User created!')
don't forget to call to
super()
.
- on the
src/users
create new folderstyles
. - in this folder create new
css
fileuser.css
.
/* src/users/styles/item.css */
.item{
width: 300px;
border-radius: 10px;
border: solid 1px lightgray;
padding: 10px;
}
- in
User
component, add the itemcss
/* src/users/components/User.js */
import React, { Component } from 'react'
import '../style/item.css'
...
- in
App.js
file, import theUser
element.
/* src/App.js */
import User from 'users/components/User'
- remove all the
jsx
that return from therender
function and return theUser
Element
/* src/App.js */
...
render() {
return (
<User/>
)
}
...
- declare the properties that
User
component going to use by addingpropTypes
.- add at the beginning of
User
file:import PropTypes from 'prop-types';
- and on the ending of the file:
User.propTypes = { id: PropTypes.number.isRequired, username: PropTypes.string.isRequired, email: PropTypes.string.isRequired }
note: this process isn't necessary but it's best practice.
- add at the beginning of
- on the
User.js
file change the hard coded id, username and email to the one that came from the prop
render() {
return (
<div className="user-item">
<div>
id:{this.props.id}
</div>
<div>
username:{this.props.username}
</div>
<div>
email:{this.props.email}
</div>
</div>
);
}
- on the
App.js
file add the id, username and email to theUser
element.
<User id={1} username={'johndoe'} email={'[email protected]'}/>
- copy the
User.js
file toEditUser.js
in the same folder (src/components
). - change the class name, in the
EditUser.js
fromUser
toEditUser
. - on
EditUser
component declareprop
in theconstructor
and pass it to thesuper
. after that assign tothis.state
the prop that you gonna use in the component
constructor(prop) {
super(prop);
this.state = {
id: prop.defaultId,
username: prop.defaultUsername,
email: prop.defaultEmail,
}
}
DON'T assign
this.state = prop
this will made problem when we try to change the prop form the parent component.
- change the
PropTypes
definition.
EditUser.propTypes = {
defaultId: PropTypes.number,
defaultUsername: PropTypes.string,
defaultEmail: PropTypes.string,
}
- add
<button>
element to the returningrender
functionjsx
<button className={'btn btn-primary'}>Save</button>
- in that
<button>
element addonClick
property and assign it to thethis.save
<button className={'btn btn-primary'} onClick={this.save}>Save</button>
- add new function to the
EditUser
class component named itsave
- on that function call to
this.setState({ userName: 'New User Name'})
...
save() {
this.setState({
userName: 'New User Name'
})
}
...
warning: when calling from
jsx
element to class function the scope changing, that why you need to bind the function to the class. i'm using the ES6fat arrow function
like so... save = () => { this.setState({ userName: 'New User Name' }) } ...
- change the render
jsx
to point to thethis.state
and not tothis.prop
return (
<div className="user-item">
...
<div>
id:{this.state.id}
</div>
<div>
username:{this.state.username}
</div>
<div>
email:{this.state.email}
</div>
...
</div>
)
- add the
EditUser
jsx element to theApp
component
<EditUser id={1} username={'johndoe'} email={'[email protected]'} />
- on
EditUser
component change the renderjsx
username and email to input element. - assign the value of the email element to
value={this.state.email}
. - assign
update
function toonChange
property. - add
name
attributename='email'
- add className '
className=form-control'
. - do this also to the
username
as well.
render() {
return (
<div className="user-item">
<div>
id:{this.state.id}
</div>
<div>
username: <input value={this.state.username} className='form-control' name='username' onChange={this.update} />
</div>
<div>
email: <input value={this.state.email} className='form-control' name='email' onChange={this.update} />
</div>
<button className={'btn btn-primary'} onClick={this.save}>Save</button>
</div>
);
}
- add
update
function to theEditUser
component class.
...
update = (event) => {
let change = {};
change[event.target.name] = event.target.value
this.setState({
...change
});
}
...
- copy the
render
return function. - delete the
User
class. - create new function name
User
and paste therender
return function. - in the class, on the argument of the function, declare
props
. - remove all of the
this
from from the pastedrender
return.
export default function User(props) {
return (
<div className="user-item">
<div>
id:{props.id}
</div>
<div>
username:{props.username}
</div>
<div>
email:{props.email}
</div>
</div>
);
}
Pro Tip: you can still define the
propTypes
just like you did in the class.
- on the
EditUser
component create newpropTypes
callsave
of type ofPropTypes.func.isRequired
- continue in the
EditUser
component change the functionsave
to callthis.prop.save
with the id, username and the email that in the state.
save = () => {
this.props.save({
id: this.state.id,
username: this.state.username,
email: this.state.email,
})
}
info: don't use spread operator in this case case there is the
save
function property.
- in the constructor of the
App
component add a new state, focusUser.
...
constructor() {
super();
this.state = {
focusUser: {
id: 1,
username: 'johndoe',
email: '[email protected]',
}
}
}
...
- create new function on the
App
class callsave
. this function will get a new user andsetState
thefocusUser
with the user using ES6 spread operator.
save = (user) => {
this.setState({
focusUser: {...user}
})
}
info: it's important to use spread operator to lose ref object with the incoming argument.
- on the
render
function change theEditUser
andUser
elements to get the id, username and email fromthis.state.focusUser
.
<EditUser id={this.state.focusUser.id} username={this.state.focusUser.username} email={this.state.focusUser.email} />
<User id={this.state.focusUser.id} username={this.state.focusUser.username} email={this.state.focusUser.email} />
- in the
EditUser
element add save property that call{this.save}
<EditUser ... save={this.save} ... />
- go to
App
component and in the constructor clear thefocusUser
value and createusers
state with users.
constructor() {
super();
this.state = {
focusUser: void 0,
users: [
{
id: 1,
username: 'johndoe',
email: '[email protected]',
},
{
id: 2,
username: 'janedoe',
email: '[email protected]',
},
{
id: 3,
username: 'johnsmith',
email: '[email protected]',
},
{
id: 4,
username: 'janesmith',
email: '[email protected]',
}
]
};
}
- in the
render
function create newlet
variable calleditTag
. - check if
this.state.focusUser
exist, if so assign theeditTag
to newEditUser
with the properties fromthis.state.focusUser
if not assign empty string.
let focusUser = this.state.focusUser;
let editTag = focusUser ?
<EditUser key={focusUser.id} save={this.save} defaultId={focusUser.id} defaultUsername={focusUser.username} defaultEmail={focusUser.email} />
: '';
- in the
render
return function replace theEditUser
element with carly braces and theeditTag
variable.
<div>
<h1>User Admin</h1>
<div className='container-fluid'>
{editTag}
</div>
</div>
- continue, using curly braces creating
User
element base onthis.state.users
.
<div>
<h1>User Admin</h1>
<div className='container-fluid'>
{editTag}
{this.state
.users
.map((user, i) =>
<User
key={i}
id={user.id}
username={user.username}
email={user.email} />)
}
</div>
</div>
note: both practices are fine. you can do what is more readable for you.
- on
App
component addfocus
function with id as parameter, find the user from thethis.state.users
andsetState
thefocusUser
with the user that found in theusers
array
focus = (id) => {
let focusUser = this.state.users.filter((u) => u.id === id)[0]
this.setState({
focusUser: { ...focusUser }
})
}
- go to
User
component and add in thepropTypes
selected
andonClick
properties.
User.propTypes = {
id: PropTypes.number.isRequired,
username: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
selected: PropTypes.bool,
onClick: PropTypes.func.isRequired,
}
- stay on the
User
component and on the return function add to the wrapper divonClick
with theprops.onClick
.
return (
<div onClick={props.onClick.bind(this, props.id)} className="user-item">
...
);
note: this time using the function
bind
function to define the context, also passingprops.id
so when the function will call it will pass the user id in the arguments
- add to the
className
property a condition, whenprops.selected
istrue
then addselected
css class.
return (
<div onClick={props.onClick.bind(this, props.id)}
className={'user-item ' + (props.selected ? 'selected' : '')}>
...
);
- go back to
App
component add to theUser
element theonClick
andselected
properties
return (
<div>
<h1>User Admin</h1>
<div className='container-fluid'>
{editTag}
{this.state
.users
.map((user, i) =>
<User
onClick={this.focus}
selected={focusUser && focusUser.id === user.id}
...
/>)
}
</div>
</div>
);
- on the
save
function find the user, that send to update, from thethis.state.users
and update the user in the array.
save = (updateUser) => {
let users = this.state.users.map((u) => {
if (u.id === updateUser.id) {
return {
...updateUser
}
} else {
return u;
}
})
this.setState({
users: users
})
}
- go to
App.css
and add css style for.user-item.selected
.user-item.selected {
background-color: lightblue;
}
- on
src/components
create new file callUserList.js
. - on that file import
React
fromreact
andexport
functionUserList
import React from 'react';
export default function UserList(props) {
}
- return in this function new empty component
React.Fragment
. - on the beginning of add
<hr/>
tag - after that add a
p
tag that print the number of user, do that usingReact.Children.count
. - continue by mapping, using curly braces, the
props.children
so that even user will wrap by div with classeven
and odd with classodd
.
return (
<React.Fragment>
<hr/>
<p>there are {React.Children.count(props.children)} users.</p>
<div className='list-group'>
{props.children.map((user, i) =>
<div key={i} className={(i % 2 === 0 ? 'even' : 'odd')}>
{user}
</div>
)}
</div>
</React.Fragment>
)
- on
App
component import the newUserList
component.
import UserList from './components/UserList';
- wrap the users mapping with the new
UserList
tag.
<UserList>
{this.state
.users
.map((user, i) =>
<User
onClick={this.focus}
selected={focusUser && focusUser.id === user.id}
key={i}
id={user.id}
username={user.username}
email={user.email}/>)
}
</UserList>
- on the
App.css
file add the css for odd class
.odd > div {
background: lightgrey;
}
- install
react-router-dom
and save it on thepackage.json
npm i -S react-router-dom
- on the
App
component addBrowserRouter
,Route
andSwitch
to the file
Pro Tip: use
BrowserRouter as Router
to map theBrowserRouter
toRouter
variable (best practice).
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
- on the
render
function remove theeditTag
, copy the<UserList>
tag (with it's nested element). - under the
<div className='container-fluid'>
add the<Router>
,<Switch>
. - create two
<Route>
element. one for the<EditUser>
element and one for the<User>
element. - on the
<Route>
element addpath
<div className='container-fluid'>
<Router>
<Switch>
<Route path='/edit/:id'/>
<Route path='/users'>
</Route>
</Switch>
</Router>
</div>
- open and close the users
<Route>
and take the<UserList>
you copy and paste as nested element.
<div className='container-fluid'>
<Router>
<Switch>
<Route path='/edit/:id'/>
<Route path='/users'>
<UserList>
{this.state
.users
.map((user, i) =>
<User
onClick={this.focus}
selected={focusUser && focusUser.id === user.id}
key={i}
id={user.id}
username={user.username}
email={user.email}/>)
}
</UserList>
</Route>
</Switch>
</Router>
</div>
- create new function in the
App
component callbindEditUserById
, passprops
as argument.
bindEditUserById = (props) => {
}
- check if
props.match.params.id
is aNumber
. if not return empty string. - filter, from
this.state.users
, the user with that id. if there isn't return empty string.
bindEditUserById = (props) => {
let id = Number(props.match.params.id;
if (!Number(id)) return ''
let focusUser = this.state.users.filter((u) => u.id === id)[0];
if (!focusUser) return ''
}
- create new function call
afterSave
and pass argumentupdateUser
. on that function call tothis.save(updateUser)
and after that call toprops.history.push('/users')
.
bindEditUserById = (props) => {
let id = Number(props.match.params.id;
if (!Number(id)) return ''
let focusUser = this.state.users.filter((u) => u.id === id)[0];
if (!focusUser) return ''
let afterSave = (updateUser) => {
this.save(updateUser);
props.history.push('/users');
}
}
- return the
<EditUser>
element with thekey
,defaultId
,defaultUsername
anddefaultEmail
properties from the filter user and for thesave
property pass theafterSave
.
bindEditUserById = (props) => {
let id = Number(props.match.params.id;
if (!Number(id)) return ''
let focusUser = this.state.users.filter((u) => u.id === id)[0];
if (!focusUser) return ''
let afterSave = (updateUser) => {
this.save(updateUser);
props.history.push('/users');
}
return <EditUser key={focusUser.id} save={afterSave} defaultId={focusUser.id} defaultUsername={focusUser.username} defaultEmail={focusUser.email} />
}
- on the
render
return function in the edit<Route>
add new property callcomponent
and pass it thethis.bindEditUserById
<div className='container-fluid'>
<Router>
<Switch>
<Route path='/edit/:id' component={this.bindEditUserById} />
...
</Switch>
</Router>
</div>
- import
Link
component fromreact-router-dom
.
import { ..., Link } from 'react-router-dom';
- wrap the
<User>
element inside the<UserList>
component with<Link>
and addto
property toedit
plus the user id
<Link to={'/edit/' + user.id} key={i}>
<User ... />
</Link>
warning: don't forget to change the
key
property from the<User>
to the<Link>
element.
- import
Redirect
component fromreact-router-dom
.
import { ..., Redirect } from 'react-router-dom';
- add
<Redirect>
component to the<Switch>
element. - in the
<Redirect>
add two properties,from='/'
andto='users'
<Redirect from='/' to='/users' />
- create new component in
src/components
folder callNotFound
import React from 'react';
export default function NotFound(props) {
return (
<React.Fragment>
<h2>404 page not found</h2>
</React.Fragment>
);
}
- import
Link
component fromreact-router-dom
.
import { Link } from 'react-router-dom';
- add
<Link>
element withto
property to theusers
page
import React from 'react';
import { Link } from 'react-router-dom';
export default function NotFound(props) {
return (
<React.Fragment>
<h2>404 page not found</h2>
<Link to={'/users'}> Go Back</Link>
</React.Fragment>
);
}
- back to the
App
component importNotFound
component.
import NotFound from './components/NotFound';
- on the
bindEditUserById
function, replace the return empty string with the<NotFound>
component
bindEditUserById = (props) => {
let id = Number(props.match.params.id);
if (!Number(id)) return <NotFound />
let focusUser = this.state.users.filter((u) => u.id === id)[0];
if (!focusUser) return <NotFound />
...
}
- add to the
<Switch>
element in the return of the render function<Route>
to the<NotFound>
component
<Switch>
...
<Route component={NotFound} />
</Switch>
warning: put that route as the last element (the
<Switch>
component related on the ordering of his children).