cd ../ # presuming still in previous lab
cd lab-04
yarn start
While we were working on the last lab, the rest of the team was adding lots of new stuff to the app
Before proceeding, let’s look at the progress that has been made:
connect
, mapDispatchToProps
, mapStateToProps
for exampleWhat will we do?
export const LIST = 'LIST_EMPLOYEES';
export const GET = 'GET_EMPLOYEE';
Redux Thunk typically works this way:
Now open src/actions/EmployeeActionCreator.js so we can create the actions.
We’ll import the types we just created, and the Axios library to handle our http requests.
import * as EmployeeActionTypes from './EmployeeActionTypes';
import Axios from 'axios';
export const list = employees => {
return {
type: EmployeeActionTypes.LIST,
employees: employees,
};
};
export const get = employee => {
return {
type: EmployeeActionTypes.GET,
employee: employee,
};
};
const apiUrl = '/api/users';
const url = employeeId => {
if (employeeId) {
return `${apiUrl}/${employeeId}`;
}
return apiUrl;
};
export const listEmployees = () => {
return dispatch => {
return Axios.get(url())
.then(response => {
dispatch(list(response.data));
console.log('Employees retrieved.');
})
.catch(error => {
console.log('Error attempting to retrieve employees.', error);
});
};
};
export const getEmployee = id => {
return dispatch => {
return Axios.get(url(id))
.then(res => {
dispatch(get(res.data));
return true;
})
.catch(error => {
console.log('There was an error getting the employee');
});
};
};
export const updateEmployee = employee => {
return dispatch => {
return Axios.put(url(employee._id), employee)
.then(res => {
dispatch(get(res.data));
console.log(`Employee : ${employee._id}, updated.`);
return true;
})
.catch(error => {
console.log('There was an error updating employee.');
});
};
};
export const removeEmployee = employee => {
return dispatch => {
employee.deleted = true;
return Axios.put(url(employee._id), employee)
.then(res => {
dispatch(get(res.data));
console.log(`Employee : ${res.data._id}, was deleted.`);
return true;
})
.catch(error => {
console.log('Error attempting to delete employee.');
});
};
};
export const restoreEmployee = employee => {
return dispatch => {
employee.deleted = false;
return Axios.put(url(employee._id), employee)
.then(res => {
dispatch(get(res.data));
console.log(`Employee : ${res.data._id}, was restored.`);
return true;
})
.catch(error => {
console.log('Error attempting to restore employee.');
});
};
};
export const createEmployee = employee => {
return dispatch => {
return Axios.post(url(), employee)
.then(res => {
dispatch(get(res.data));
console.log(`Employee : ${res.data._id}, created.`);
return true;
})
.catch(error => {
console.log('There was an error creating employee.');
});
};
};
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import * as actions from './EmployeeActionCreator';
import * as types from './EmployeeActionTypes';
import moxios from 'moxios';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('synchronous actions', () => {
it('list should send objects', () => {
expect(actions.list(['p1', 'p2'])).toEqual({
type: types.LIST,
employees: ['p1', 'p2'],
});
});
it('get should send object', () => {
expect(actions.get('p1')).toEqual({
type: types.GET,
employee: 'p1',
});
});
});
describe('async actions', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
it('creates LIST when fetching employees has been done', () => {
moxios.stubRequest('/api/users', {
status: 200,
response: ['employee1', 'employee2'],
});
const expectedActions = [
{ type: types.LIST, employees: ['employee1', 'employee2'] },
];
const store = mockStore({ employees: [] });
return store.dispatch(actions.listEmployees()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
it('create GET when requesting single employee', () => {
moxios.stubRequest('/api/users/1', {
status: 200,
response: 'employee1',
});
const expectedActions = [{ type: types.GET, employee: 'employee1' }];
const store = mockStore({ employees: [] });
return store.dispatch(actions.getEmployee(1)).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
it('create GET when updating a employee', () => {
moxios.stubRequest('/api/users/1', {
status: 200,
response: 'employee1',
});
const expectedActions = [{ type: types.GET, employee: 'employee1' }];
const store = mockStore({ employees: [] });
return store.dispatch(actions.updateEmployee({ _id: 1 })).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
it('create GET when removing a employee', () => {
moxios.stubRequest('/api/users/1', {
status: 200,
response: 'employee1',
});
const expectedActions = [{ type: types.GET, employee: 'employee1' }];
const store = mockStore({ employees: [] });
return store.dispatch(actions.removeEmployee({ _id: 1 })).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
it('create GET when restoring a employee', () => {
moxios.stubRequest('/api/users/1', {
status: 200,
response: 'employee1',
});
const expectedActions = [{ type: types.GET, employee: 'employee1' }];
const store = mockStore({ employees: [] });
return store.dispatch(actions.restoreEmployee({ _id: 1 })).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
});
Let’s take a look at what we are doing here. - First we’re testing that the synchronous actions are creating packets of type LIST and GET. - Then in the asynchronous tests we are using moxios to http response for our axois requests, and we’re using the redux-mock-store’s configureMockStore to mock our redux store so we can dispatch or actions and test them. - Finally, we are using those mocks to see that the async actions generate the packets we expect when dispatched.
Make sure the tests pass and move on to the next section.
import * as EmployeeActionTypes from '../actions/EmployeeActionTypes';
export default (state = { data: [] }, action) => {
switch (action.type) {
case EmployeeActionTypes.LIST:
return { ...state, data: action.employees };
case EmployeeActionTypes.GET:
const updatedItem = action.employee;
const index = state.data.findIndex(d => d._id === updatedItem._id);
if (index >= 0 ) {
const copy = [...state.data];
copy.splice(index, 1, updatedItem);
return { ...state, data: copy };
}
return { ...state, data: [...state.data, updatedItem] };
default:
return state;
}
};
When the reducer receives a LIST action it will replace the currently-stored list of employees with the new one from the action’s payload
When a GET is received the reducer will attempt to replace the existing version of that employee in state, or will add to the end of the list if it’s a new item
Now let’s add our employee-reducer to the combined reducer
Open src/reducers/index.js
Import the employee-reducer
import employees from './employee-reducer';
const rootReducer = combineReducers({
projects: projects,
timesheets: timesheets,
timeunits: timeunits,
employees: employees,
});
import { connect } from 'react-redux';
import * as EmployeeActionCreators from '../actions/EmployeeActionCreator';
const mapStateToProps = state => {
return {
employees: state.employees.data,
};
};
const mapDispatchToProps = {
listEmployees: EmployeeActionCreators.listEmployees,
deleteEmployee: EmployeeActionCreators.removeEmployee,
restoreEmployee: EmployeeActionCreators.restoreEmployee
};
export default connect(mapStateToProps, mapDispatchToProps)(Employees);
componentDidMount() {
const { listEmployees } = this.props;
listEmployees();
}
This allows the following to happen:
listEmployees
activitymapStateToProps
- this will take the data from Redux and pass it into our component as a propNow we update the data we are passing to the EmployeeTable in the render method. Note that we’re now pulling employees from props since React-Redux pulls them from global Redux state and adds them to the components props in mapStateToProps
We also pass down two of the actions we’re getting from mapDispatchToProps
so that they can be called from a row
const { employees, deleteEmployee, restoreEmployee } = this.props;
return (
<div>
<h1>Employees</h1>
<EmployeeTable employees={ employees } onDelete={deleteEmployee} onRestore={restoreEmployee} />
</div>
);
const { employees, onDelete, onRestore } = this.props;
...
{employees.map(employee => (
<EmployeeRow employee={ employee } key={ employee._id } onDelete={onDelete} onRestore={onRestore} />
))}
EmployeeTable.propTypes = {
employees: PropTypes.array.isRequired,
onDelete: PropTypes.func,
onRestore: PropTypes.func
};
<th>Delete</th>
Now let’s open src/employees/EmployeeRow.js and add the delete/restore functionality
Import the Bootstrap Button component
import { Button } from 'react-bootstrap';
<td>
<Button onClick={this.handleClick} bsStyle={employee.deleted ? 'success' : 'danger'}>
{employee.deleted ? 'Restore' : 'Delete'}
</Button>
</td>
<tr className={employee.deleted ? 'deleted' : ''}>
handleClick = (event) => {
const { employee, onDelete, onRestore } = this.props;
if (employee.deleted) {
onRestore(employee);
} else {
onDelete(employee);
}
event.stopPropagation();
};
EmployeeRow.propTypes = {
employee: PropTypes.object.isRequired,
onDelete: PropTypes.func.isRequired,
onRestore: PropTypes.func.isRequired
};
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'react-bootstrap';
class EmployeeRow extends React.Component {
handleClick = (event) => {
const { employee, onDelete, onRestore } = this.props;
if (employee.deleted) {
onRestore(employee);
} else {
onDelete(employee);
}
event.stopPropagation();
};
render() {
const { employee } = this.props;
return (
<tr className={employee.deleted ? 'deleted' : ''}>
<td>{employee.username}</td>
<td>{employee.email}</td>
<td>{employee.firstName}</td>
<td>{employee.lastName}</td>
<td>{employee.admin ? 'Yes' : 'No'}</td>
<td>
<Button onClick={this.handleClick} bsStyle={employee.deleted ? 'success' : 'danger'}>
{employee.deleted ? 'Restore' : 'Delete'}
</Button>
</td>
</tr>
);
}
}
EmployeeRow.propTypes = {
employee: PropTypes.object.isRequired,
onDelete: PropTypes.func.isRequired,
onRestore: PropTypes.func.isRequired
};
export default EmployeeRow;
If you haven’t already done so,
yarn start
to fire off the build.Did your application display any data? Look at your console to see the reported “Proxy error”. This gives us a clue that we need to start our backend server to start supplying the data to our application.
yarn start
frontend running, but now open a 2nd console window—or a separate tabCheckout the backend API server project from Github.
$ git clone https://github.com/objectpartners/react-redux-api.git
You should get output similar to below:
Cloning into 'react-redux-api'...
remote: Counting objects: 6272, done.
remote: Compressing objects: 100% (2493/2493), done.
remote: Total 6272 (delta 3534), reused 6260 (delta 3523), pack-reused 0
Receiving objects: 100% (6272/6272), 2.31 MiB | 4.05 MiB/s, done.
Resolving deltas: 100% (3534/3534), done.
Checking connectivity... done.
Change directories into the API main directory.
$ cd react-redux-api
Install the backend API NPM dependencies
$ yarn
Run the backend API server
$ yarn start
git add .
git commit -m "I think I know redux now?!"