In this project we will take a look at an employee management application and learn how it works. We will cover the following topics, found below in the list, by going through each project stage and building out a part of the application.
- State
- Props
- JavaScript Classes
- .bind
- this
- componentDidUpdate ( React life cycle method )
We can control which stage we are on by using index.js
in the src/
directory. On line 3 in src/index.js
you should see:
import App from './stage-1/App';
We can change stages by changing the number in the string. For example if I wanted stage 2, I would do:
import App from './stage-2/App';
It's imperative to change the stage number in src/index.js
when moving from
stage to stage in this README!
Also, in this project the stages will build on top of each other. Every stage
will have you repeat the process of the last stage(s). Try to do the previous
stage(s) steps from memory if possible and re-visit their detailed instructions
if you get lost. Files containing the solution can be found on the solution
branch.
Live Example: https://boomcamp.github.io/react-2
Fork
andclone
thisrepository- Run
npm install
in the root directory - Run
npm start
to spin up a development server ( keep the development server running to debug stages )
In this stage we will fix context issues using .bind
and this
. If we inspect
our application, we can see that when we try to interact with the components
nothing is working correctly and we are getting an error that this.setState
is
not a function.
Using the browser's developer tools, figure out where .bind
needs to be
applied in App.js
and EmployeeList.js
.
Detailed Instructions
- Open
src/stage-1/App.js
. - Open
src/stage-1/components/EmployeeEdtior.js
.
The first error that you should encounter is when clicking on an employee. This
error is happening when the selectEmployee
method on App
gets called from
the employeeList
component. What's happening here? We're losing the context of
this
when the method is called.
First let's cover the data flow to figure out why our context is getting lost.
Inside of App.js
we can see on line 56 we are adding the selectEmployee
prop
to our EmployeeList
component. The selectEmployee
method is being passed
from our App
class. This means that inside of the employeeList
component it
can access the method through this.props.selectEmployee
. We are then using the
selectEmployee
prop on line 14 in EmployeeList.js
in combination with an
onClick
event.
Because of this current setup when the selectEmployee
method gets called from
the employeeList
component this
does not refer to the App
class which has
a the setState
method. this
refers to the props on the EmployeeList
component. We can prove that by adding a console.log(this)
before
this.setState({})
gets called in the selectEmployee
method. The log should
look similiar to:
{
employees: [],
selectEmployee: function
}
So if the App
component has the method of setState
how can we keep our
context of this
when calling the method in EmployeeList
? We can bind
it
when the context of this
equals the App
component. In App.js
at the bottom
of the constructor
method we can bind
this
to our class method.
constructor() {
super();
this.state = {...};
// binding the context here
this.selectEmployee = this.selectEmployee.bind(this);
}
Now our selectEmployee
method should be working properly and updating the
EmployeeEditor
component on the right.
The next error we should encounter is that the save
and cancel
buttons in
the EmployeeEditor
component are not working. Based on the error message in
the browser debugger, it appears that this
is equal to null
when inside of
the save
and cancel
methods. Since state exists on the component, we want to
use bind
when this
equals the component. Just like with the selectEmployee
method we can bind
this
to the save
and cancel
methods at the bottom of
the constructor
method in the EmployeeEditor
component.
constructor() {
super();
this.state = {
employee: null,
originalEmployee: null,
notModified: true
};
// binding the context here
this.save = this.save.bind(this);
this.cancel = this.cancel.bind(this);
}
This will fix our cancel
button context issue however you'll notice that
save
still has a context issue. This is because it calls a method passed down
as a prop called refreshList
. refreshList
handles
updating the EmployeeList
names on the left hand side. If we add a console.log(this)
in the refreshList
method we'll see it has a similiar issue of this
referring
to the object of props. If we .bind
this
to the method at the bottom of the constructor
method in App.js
, just like we did for selectEmployee
, then this
will have
the correct context.
constructor() {
super();
this.state = {...};
this.selectEmployee = this.selectEmployee.bind(this);
// binding context here
this.refresh = this.refresh.bind(this);
}
Remember to change your to import from src/stage-2
. You'll need to
reimplement the previous steps, try to do it from memory.
In this stage we will re-create our componentDidUpdate
life cycle method in
the EmployeeEditor
component. This life cycle method handles updating our
state
in EmployeeEditor.js
when the selected
prop gets updated from the
EmployeeList
component.
Create a componentDidUpdate
method in EmployeeEditor.js
that has one
parameter: prevProps
. The method should be written after the constructor
method and will update the following state
properties using setState
:
employee
, originalEmployee
, and notModified
. employee
should be updated
to a copy of the selected
object from props
, originalEmployee
should be
updated to the selected
object from props
, and notModified
should be
updated to true
.
Detailed Instructions
Open EmployeeEditor.js
from src/stage-2/components/EmployeeEditor.js
and
look for the // componentDidUpdate
comment. Let's create our
componentWillReceiveProps
method there with one parameter called prevProps
.
componentDidUpdate(prevProps) {
}
This life cycle method will be called whenever the props
for EmployeeEditor
get updated after the initial render. We'll use the parameter prevProps
to
have access to the previouse props object and use it to compare prop values to
decide if we need to update the state with this.setState
. Remember that we
want to update employee
and originalEmployee
on state with the selected
prop and update notModified
to true
. We also want to make sure that
employee
is a copy of the selected
object. Since our EmployeeEditor
component is only rendered with two props
, our prevProps
parameter in
componentDidUpdate
will look like:
{
selected: { } // This is an object of 1 employee ( the one that was selected from the list ), also can be null if it's not set.
refreshList: function // This is a method from App.js that will refresh the list of employees
}
Let's dive into why we are using employee
and originalEmployee
, or in other
words why a copy and a original of the same object. In JavaScript, if I set a
variable equal to an already defined object they both reference the same object.
For example:
var obj1 = {
name: 'James',
};
var obj2 = obj1;
obj2.name = 'Override';
console.log(obj1.name); // 'Override'
Even though I created a new variable, obj2
and changed the name
property on
obj2
, obj2
and obj1
's name
property was updated to 'Override'
. This
would be bad for an onChange
event that updates a state property every time a
user types because we don't want changes to be final until the user presses the
Save
button. To get around this issue, we can use Object.assign()
to make a
copy of an object into a new object; effectively separating the two.
var obj1 = {
name: 'James',
};
var obj2 = Object.assign({}, obj1);
obj2.name = 'Override';
console.log(obj1.name); // 'James'
There is a down side to Object.assign
in this scenario however, which is why
we are also using a state property called originalEmployee
. When using
Object.assign
, we only get a copy of the properties on the object but not the
prototypes
. We will need those prototypes
for a later stage so we'll just
use originalEmployee
to store the original object. As a bonus, since we have
an original copy of the object, canceling changes is as easy as setting
employee
equal to a copy of originalEmployee
.
So getting back to the problem at hand, our componentDidUpdate
method. Let's
use this.setState
to update employee
to be a copy of this.props.selected
,
originalEmployee
to be this.props.selected
, notModified
to be true. We
reset notModified
to true so when a user selects a new employee the Save and
Cancel button wont be enabled until they make a change again.
componentDidUpdate(prevProps) {
// checking this condition so we don't update unless there was a change.
// if we don't do this we could cause an infinite loop.
if (prevProps.selected !== this.props.selected) {
this.setState({
employee: Object.assign({}, this.props.selected),
originalEmployee: props.selected,
notModified: true
});
}
}
rember to update index.js
with the correct stage
In this stage we will re-create our handleChange
method in
EmployeeEditor.js
.
Create a handleChange
method on the EmployeeEditor
component that takes in
what property to change and what value to give that property as parameters. Also
we want to update notModified
on state from true
to false
since a
modification has occured.
Detailed Instructions
Open EmployeeEditor.js
in src/stage-3/components/EmployeeEditor.js
and look
for the // handleChange
comment. Let's create the skeleton of the method
there.
handleChange(propName, val) {
}
In this method, we'll want to change the notModified
property on state from
true
to false
. When we update notModified
to false
the Save and Cancel
buttons will no longer be disabled ( allowing a user to click on them ). We also
only need to update this property if it is true
, so let's add an if statement
to wrap our setState
call.
handleChange(propName, val) {
if ( this.state.notModified ) {
this.setState({ notModified: false })
}
}
Next, we'll want our method to update the employee
object on state, we should
never mutate state directly so we'll have to make a copy of our employee
property on state before modifying it and finally updating it with
this.setState
. Lets create a variable called employeeCopy
that equals a copy
of the employee
property on state.
handleChange(propName, val) {
if ( this.state.notModified ) {
this.setState({ notModified: false })
}
var employeeCopy = Object.assign({}, this.state.employee);
}
We can then use bracket notation to take our string, propName
, to access that
property on the employeeCopy
object and update its value with val
.
handleChange(prop, val) {
if ( this.state.notModified ) {
this.setState({ notModified: false })
}
var employeeCopy = Object.assign({}, this.state.employee);
employeeCopy[prop] = val;
}
Then to update state is as easy as updating employee
to our employeeCopy
variable.
handleChange(propName, val) {
if ( this.state.notModified ) {
this.setState({ notModified: false })
}
var employeeCopy = Object.assign({}, this.state.employee);
employeeCopy[propName] = val;
this.setState({ employee: employeeCopy });
// remember, we never directly modify state, it's always updated through the '.setState' method
// this is how React can perform the update in a consistent way.
}
Don't forget to update index.js
for this stage.
In this stage we will re-create our Employee
model in Employee.js
.
Create a class called Employee
in src/stage-4/models/Employee.js
. This class
should have a constructor method that takes an id
, name
, phone
, and
title
parameter. It should then assign those onto the class. This class should
also have three methods: updateName
, updatePhone
, and updateTitle
. Each
method will take in a string as a parameter and then update the corresponding
property on the class with the string.
Detailed Instructions
Open Employee.js
in src/stage-4/models/Employee.js
. We'll start by adding
our constructor
method where the // constructor
comment is. This method gets
called when a new Employee
class is created. We'll be making new employees
with four items. An id
, a name
, a phone
, and a title
. Therefore we'll
want to make a constructor
method with four parameters to capture those items.
constructor(id, name, phone, title) {
}
Then we can assign them to the class by using this
.
constructor(id, name, phone, title) {
this.id = id;
this.name = name;
this.phone = phone;
this.title = title;
}
Now all we need are three methods, one to update the name
, one to update the
phone
, and one to update the title
. For simplicity, I broke these out into
three methods, they'll be very similiar to each other. All three methods will
take in a string
parameter and then update the corresponding property on the
class to that string
.
constructor(id, name, phone, title) {
this.id = id;
this.name = name;
this.phone = phone;
this.title = title;
}
updateName(name) {
this.name = name;
}
updatePhone(phone) {
this.phone = phone;
}
updateTitle(title) {
this.title = title;
}
Don't forget to update the index.js
for this stage.
In this stage we will re-create our save
and cancel
methods in the
EmployeeEditor
component.
Create a save
method after the handleChange
method that calls all three
update
methods on the Employee
model. Remember that originalEmployee
has
access to the methods and employee
doesn't because it is a copy. Use the
values on state
when calling the update
methods. This method should also set
notModified
on state to true
and finally call the refreshList
method off
of props. Then create a cancel
method after the save
method that updates
employee
to a copy of the originalEmployee
and updates notModified
to
true
on state.
Detailed Instructions
Open EmployeeEditor.js
in src/stage-5/components/EmployeeEditor.js
. Let's
begin with our save
method. Look for the // save
comment and let's create
our save
method skeleton.
save() {
}
In this save
method, we will want to use the prototypes
on the Employee
class to update our values. Since there are only three properties to update
let's just call all three update methods regardless of which one has changed. It
would be more code to check which one to update.
save() {
this.state.originalEmployee.updateName(this.state.employee.name);
this.state.originalEmployee.updatePhone(this.state.employee.phone);
this.state.originalEmployee.updateTitle(this.state.employee.title);
}
Then we'll need to set notModified
back to true
since we have now updated
that employee and no modifications have happened since saving. We'll also need
to call refreshList
so that our EmployeeList
will re-render with the name
change if there is one.
save() {
this.state.originalEmployee.updateName(this.state.employee.name);
this.state.originalEmployee.updatePhone(this.state.employee.phone);
this.state.originalEmployee.updateTitle(this.state.employee.title);
this.setState({ notModified: true });
this.props.refreshList();
}
Now let's create our cancel method skeleton where the // cancel
comment is.
cancel() {
}
In this method, we'll want to make a copy of the this.props.selected
(the
originally selected employee) object. Then we'll use setState
to update our
employee
by using Object.assign()
, and also set notModified
to true
.
cancel() {
this.setState({
employee: Object.assign({}, this.props.selected),
notModified: true
});
}
Don't forget to update index.js
for this stage.
In this stage we will re-create our selectEmployee
and refresh
methods on
the App
component.
Create a selectEmployee
method after the constructor
method that takes an
employee
as a parameter. The method should then use setState
to update the
selectedEmployee
property on state to the passed in employee
. Then create a
refresh
method after the selectedEmployee
method. This method should just
call setState
with the argument of this.state
.
Detailed Instruction
Open App.js
in src/stage-6/
. Let's begin with the selectEmployee
method,
look for the // selectEmployee
comment and let's create the skeleton of our
method. This method will have one parameter being the object of the employee
that was selected.
selectEmployee(employee) {
}
All we needthismethod to do is update the selectedEmployee
property on state.
We'll do that by using this.setState
.
selectEmployee(employee) {
this.setState({ selectedEmployee: employee });
}
Next let's create the skeleton of our refresh
method. Look for the
// refresh
comment.
refresh() {
}
This method is another simple one that will refresh our state, effectively
updating all our components, by calling this.setState
and passing in the
current state as the object. Since we are updating our employees using methods
on their class our state does not automatically trigger a re-render.
refresh() {
this.setState(this.state);
}
Don't forget to update index.js
for this stage.
In this stage we will re-create our constructor
methods and state in App.js
and EmployeeEditor.js
.
Create a constructor
method that calls super();
and creates an empty state
object ( this.state = {}
) in both App.js
and EmployeeEditor.js
. Then use
the bullet lists to fill in the state properties:
State properties for App.js
- employees: ( code snippet below )
- selectedEmployee: null
employees array
[
new Employee(0, 'Bernice Ortiz', 4824931093, 'CEO'),
new Employee(1, 'Marnie Barnett', 3094812387, 'CTO'),
new Employee(2, 'Phillip Weaver', 7459831843, 'Manager'),
new Employee(3, 'Teresa Osborne', 3841238745, 'Director of Engineering'),
new Employee(4, 'Dollie Berry', 4873459812, 'Front-End Developer'),
new Employee(5, 'Harriett Williamson', 6571249801, 'Front-End Developer'),
new Employee(6, 'Ruby Estrada', 5740923478, 'Back-End Developer'),
new Employee(7, 'Lou White', 8727813498, 'Full-Stack Developer'),
new Employee(8, 'Eve Sparks', 8734567810, 'Product Manager'),
new Employee(9, 'Lois Brewer', 8749823456, 'Sales Manager'),
];
State properties for EmployeeEditor.js
- employee: null
- originalEmployee: null
- notModified: true
Detailed Instructions
- Open
App.js
(src/stage-7/
) - Open
EmployeeEditor.js
(src/stage-7/components/EmployeeEditor.js
)
In App.js
look for the // constructor
comment a create the skeleton for the
constructor
method. Remember it should call super()
and then create an empty
state
object.
constructor() {
super();
this.state = {
};
}
Then using the bullet list above we can add our properties we need to state.
constructor() {
super();
this.state = {
employees: [
new Employee(0, 'Bernice Ortiz', 4824931093, 'CEO'),
new Employee(1, 'Marnie Barnett', 3094812387, 'CTO'),
new Employee(2, 'Phillip Weaver', 7459831843, 'Manager'),
new Employee(3, 'Teresa Osborne', 3841238745, 'Director of Engineering'),
new Employee(4, 'Dollie Berry', 4873459812, 'Front-End Developer'),
new Employee(5, 'Harriett Williamson', 6571249801, 'Front-End Developer'),
new Employee(6, 'Ruby Estrada', 5740923478, 'Back-End Developer'),
new Employee(7, 'Lou White', 8727813498, 'Full-Stack Developer'),
new Employee(8, 'Eve Sparks', 8734567810, 'Product Manager'),
new Employee(9, 'Lois Brewer', 8749823456, 'Sales Manager')
],
selectedEmployee: null
};
}
Now let's do the same exact steps for EmployeeEditor.js
constructor() {
super();
this.state = {
employee: null,
originalEmployee: null,
notModified: true
};
}
Don't forget to update index.js
for this stage.
In this stage we will render
our child components in App.js
.
Import the Header
, EmployeeList
, and EmployeeEditor
components into
App.js
. Then render
the Header
component nested under the div
with the
id
of app
and render
the EmployeeList
and EmployeeEditor
components
nested under the div
with the id
of main-container
.
Detailed Instructions
In src/stage-8/App.js
let's begin by importing our three components. Based on
the file structure inside of stage 8, we can see there is a components folder at
the same level of App.js
. Therefore, we will be importing our components from
'./components/'
. Let's import
our components in App.js
where it says
// Components
.
import Header from './components/Header/Header';
import EmployeeList from './components/EmployeeList/EmployeeList';
import EmployeeEditor from './components/EmployeeEditor/EmployeeEditor';
Now that App.js
has access to these components, we can then render
them.
Let's render
the Header
component nested inside of the div
with the id
of app
. And render
the EmployeeList
and EmployeeEditor
component nested
in the div
with the id
of main-container
.
return (
<div id="app">
<Header />
<div className="main-container">
<EmployeeList />
<EmployeeEditor />
</div>
</div>
);
Now we need to add the props
so our child components can still function
correctly. For EmployeeList
to function correctly, it will need two props:
employees
and selectEmployee
. employees
should equal the array of
employees kept on state in App.js
and selectEmployee
should equal the method
on App.js
that calls setState
to update the selected employee.
return (
<div id="app">
<Header />
<div className="main-container">
<EmployeeList
employees={this.state.employees}
selectEmployee={this.selectEmployee.bind(this)}
/>
<EmployeeEditor />
</div>
</div>
);
For EmployeeEditor
to function correctly it will need two props: selected
and refreshList
. selected
should equal the selectedEmployee
property on
App.js
's state and refreshList
should equal the method on App.js
that
calls setState(this.state)
.
return (
<div id="app">
<Header />
<div className="main-container">
<EmployeeList
employees={this.state.employees}
selectEmployee={this.selectEmployee.bind(this)}
/>
<EmployeeEditor
selected={this.state.selectedEmployee}
refreshList={this.refresh.bind(this)}
/>
</div>
</div>
);
Don't forget to update index.js
for this stage.
In this stage we will render
our list of employees in the EmployeeList
component by mapping over the prop employees
.
Map over this.props.employees
to return <li>
elements. Use the id
of the
employee as the key
for the element, add an onClick
to each <li>
to call
selectEmployee
with the current employee as an argument, and set the text
value of the <li>
to the name
of the employee. Also add the className
of
listText
to each <li>
element.
Detailed Instructions
Open EmployeeList.js
from src/stage-9/components/EmployeeList.js
and look
for the // Map over this.props.employees
comment.
<ul className="listContainer">
{
// Map over this.props.employees
}
</ul>
Let's remove the comment and make the skeleton for our mapping. Let's call the
parameter for the mapping's callback function employee
and return
nothing.
<ul className="listContainer">
{
this.props.employees.map((employee) => {
return (
)
})
}
</ul>
Now each item in the this.props.employee
array will be referenced in our
callback function as employee
and we can add JSX inside of our return. Let's
have our callback return a <li>
element that has a key
attribute equal to
the employee.id
and className
of listText
.
<ul className="listContainer">
{this.props.employees.map(employee => {
return <li className="listText" key={employee.id} />;
})}
</ul>
We'll also want to add an onClick
attribute that uses an arrow function that
calls the selectEmployee
method from props with the current employee
.
<ul className="listContainer">
{this.props.employees.map(employee => {
return (
<li
className="listText"
key={employee.id}
onClick={() => {
this.props.selectEmployee(employee);
}}
/>
);
})}
</ul>
And finally we want the text of the <li>
element to be the name of the
employee
.
<ul className="listContainer">
{this.props.employees.map(employee => {
return (
<li
className="listText"
key={employee.id}
onClick={() => {
this.props.selectEmployee(employee);
}}
>
{' '}
{employee.name}{' '}
</li>
);
})}
</ul>
Re-create the project from Stages 1 - 9 without looking back at code solutions. If you have to look back at a certain stage, restart from stage-1 again.