cd ../ # presuming still in previous lab
cd lab-02
yarn start
The first thing we need in our Timesheet application is a listing of all the projects that an employee can be working on. We want a table with one row for each Project.
Many times in React it’s helpful to build things starting at the bottom and building your way upwards - we’re going to do this here by building a row, then a table, then a container.
Open src/projects/ProjectRow.js
This is going to be the component that will render a single row in our table using data from a Project
The first thing we want to do is import
the libraries we need. At the top of the page add:
import React from 'react';
import PropTypes from 'prop-types';
This imports the React library so we can create React logic, and the PropTypes library so we can define the types of properties our React logic expects.
Next let’s create our empty React component class and have the module export
the class:
class ProjectRow extends React.Component {
}
export default ProjectRow;
Now we have to tell React what we want the component to draw to the page.
To do this we need to implement a render()
method:
Inside the class
, add the below method:
render() {
const { project } = this.props;
return (
<tr>
<td>{project.name}</td>
<td>{project.description}</td>
</tr>
);
}
Let’s look at what we just did:
JSX
template that builds an HTML table row along with a couple table cellsNext we declare that this component expects a single prop named ‘project’. Add this just above the default export:
ProjectRow.propTypes = {
project: PropTypes.object.isRequired
};
import React from 'react';
import PropTypes from 'prop-types';
class ProjectRow extends React.Component {
render() {
const { project } = this.props;
return (
<tr>
<td>{project.name}</td>
<td>{project.description}</td>
</tr>
);
}
}
ProjectRow.propTypes = {
project: PropTypes.object.isRequired
};
export default ProjectRow;
We have a row, but now we need a way to turn a list of projects into a table full of ProjectRow
components
Open src/projects/ProjectTable.js
Add the necessary imports:
import React from 'react';
import PropTypes from 'prop-types';
import { Table } from 'react-bootstrap';
import ProjectRow from './ProjectRow';
You’ll notice we’re pulling in two new imports:
Table
component from a third-party library called react-bootstrap
- this is just to make things look prettyProjectRow
component for each Project we want to render in our tableDeclare our base class and export
class ProjectTable extends React.Component {
}
export default ProjectTable;
render() {
const { projects } = this.props;
return (
<Table bordered striped>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{projects.map(project => (
<ProjectRow project={ project } key={ project._id }/>
))}
</tbody>
</Table>
);
}
Whoa! This is way more complicated than the last one! What’s going on here?
Table
and set some props to enable some Bootstrap-supplied styling (‘bordered’ adds a border, ‘striped’ causes alternating rows to be different colors)ProjectRow
component. We pass the project down as a prop.
Note: We’re also giving React an ‘index’ in the form of key
so it can efficiently render the list of itemsFinally, add our prop declarations:
ProjectTable.defaultProps = {
projects: []
};
ProjectTable.propTypes = {
projects: PropTypes.array
};
import React from 'react';
import PropTypes from 'prop-types';
import { Table } from 'react-bootstrap';
import ProjectRow from './ProjectRow';
class ProjectTable extends React.Component {
render() {
const { projects } = this.props;
return (
<Table bordered striped>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{projects.map(project => (
<ProjectRow project={ project } key={ project._id }/>
))}
</tbody>
</Table>
);
}
}
ProjectTable.defaultProps = {
projects: []
};
ProjectTable.propTypes = {
projects: PropTypes.array.isRequired
};
export default ProjectTable;
Awesome! We have a component that will display a table full of project data. But where does the data come from?
It’s typically best practice to separate out how you show your data (present) vs. manage your data (contain) - we’ll talk more about this when we get to Redux
Let’s build a data container
Open src/projects/Projects.js
This is going to be the component that will hold the Project data and pass it down into the table
Add the necessary imports:
import React from 'react';
import ProjectTable from './ProjectTable';
class Projects extends React.Component {
}
export default Projects;
state = {
projects: [
{
_id: 1,
name: 'Project1',
description: 'This is your first project'
},
{
_id: 2,
name: 'Project2',
description: 'This is your second project'
},
{
_id: 3,
name: 'Project3',
description: 'This is the third project'
}
]
};
render() {
const { projects } = this.state;
return (
<div>
<h1>Projects</h1>
<ProjectTable projects={projects} />
</div>
);
}
import React from 'react';
import ProjectTable from './ProjectTable';
class Projects extends React.Component {
state = {
projects: [
{
_id: 1,
name: 'Project1',
description: 'This is your first project'
},
{
_id: 2,
name: 'Project2',
description: 'This is your second project'
},
{ _id: 3, name: 'Project3', description: 'This is the third project' }
]
};
render() {
const { projects } = this.state;
return (
<div>
<h1>Projects</h1>
<ProjectTable projects={projects} />
</div>
);
}
}
export default Projects;
Now that we’ve created our first components, we need to make sure they work as expected
Open src/projects/ProjectRow.test.js
First, let’s import our libraries for React
ProjectRow
and the shallow enzyme
renderer
Then, let’s set up the test suite by adding a describe
block:
import React from 'react';
import { shallow } from 'enzyme';
import ProjectRow from './ProjectRow';
describe('<ProjectRow />', () => {
// Tests go here
});
describe
block:let wrapper;
beforeEach(() => {
const project = {
name: 'NAME',
description: 'DESCRIPTION'
};
wrapper = shallow(<ProjectRow project={project} />);
});
What is happening here? We use the shallow renderer from Enzyme to render the component into a in-memory sandboxed “document” so that we can perform inquiries. Notice that we are using
JSX
in theshallow()
method. Shallow testing is useful to isolate our test by not rendering any child components. For more advanced “integration” style tests you would usemount()
for full DOM rendering
beforeEach
block:it('should instantiate the Project Row Component', () => {
expect(wrapper).toHaveLength(1);
});
it('should render values into expected cells', () => {
expect(wrapper.find('td')).toHaveLength(2);
expect(wrapper.find('td').at(0).text()).toEqual('NAME');
expect(wrapper.find('td').at(1).text()).toEqual('DESCRIPTION');
});
import React from 'react';
import { shallow } from 'enzyme';
import ProjectRow from './ProjectRow';
describe('<ProjectRow />', () => {
let wrapper;
beforeEach(() => {
const project = {
name: 'NAME',
description: 'DESCRIPTION'
};
wrapper = shallow(<ProjectRow project={project} />);
});
it('should instantiate the Project Row Component', () => {
expect(wrapper).toHaveLength(1);
});
it('should render values into expected cells', () => {
expect(wrapper.find('td')).toHaveLength(2);
expect(wrapper.find('td').at(0).text()).toEqual('NAME');
expect(wrapper.find('td').at(1).text()).toEqual('DESCRIPTION');
});
});
import React from 'react';
import { shallow } from 'enzyme';
import ProjectTable from './ProjectTable';
import ProjectRow from './ProjectRow';
describe('<ProjectTable />', () => {
let wrapper;
beforeEach(() => {
const projects = [{ _id: 1 }, { _id: 2 }];
wrapper = shallow(<ProjectTable projects={projects} />);
});
it('should instantiate the Project Table Component', () => {
expect(wrapper).toHaveLength(1);
});
it('should render a row for each project', () => {
expect(wrapper.find(ProjectRow)).toHaveLength(2);
});
});
These tests shallow render ProjectTable with two stubbed project entries
Finally, add some tests for Projects. Open src/projects/Projects.test.js
import React from 'react';
import { shallow } from 'enzyme';
import Projects from './Projects';
import ProjectTable from './ProjectTable';
describe('<Projects />', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<Projects />);
});
it('should instantiate the Project Component', () => {
expect(wrapper).toHaveLength(1);
});
it('should pass projects down to table', () => {
wrapper.setState({
projects: [{}, {}, {}, {}]
});
expect(wrapper.find(ProjectTable).prop('projects')).toHaveLength(4);
});
});
Here, we’re shallow rendering the Projects container
yarn test
) command.
Did your tests pass?
If you get a weird error like the following, try installing watchman as reported here: watchman bug
Error: Error watching file for changes: EMFILE
at exports._errnoException (util.js:953:11)
at FSEvent.FSWatcher._handle.onchange (fs.js:1400:11)
yarn ERR! Test failed. See above for more details.
There are many more assertions that are possible!
Open src/App.js, and tell React to render our component into our app.
render()
method to place it on our page:import Projects from './projects/Projects';
render() {
return (
<div className="App">
<div className="container">
<Projects />
</div>
</div>
);
}
How does React get rendered?
App
component and renders it into a DOM node with an id of “root”. This is how React gets bootstrapped.yarn start
to fire off the build.
git add .
git commit -m "Lab 2 completed successfully"
If you’re looking for an extra challenge, take a look at Jest Snapshot testing. On first run, jest will generate a snapshot file that represents the rendered react component in a __snapshots__ folder. On subsequent runs it will compare the test result to the previous snapshot file. On a failure you will have to decide whether to fix the code, modify the test, or press ‘u’ to update the snapshot file with the new rendering.
Snapshot testing can save you time from writing individual expect assertions on elements, by simply allowing you to review the snapshot file on any changes. It also serves as a useful “tripwire” to inform developers that they may have impacted components/logic they didn’t intend to.
Try creating a Snapshot test inside ProjectRow.test.js, or:
it('should render to match the snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
git add .
and git commit -m "extra credit"
when you are done