React Redux Thunk
Setup
tip
If you previously completed the HTTP demos these three setup steps were already completed in that section.
- Verify these styles have been added to the
demos
project - Verify the backend API setup detailed here has been completed.
- Verify the items have been added to
db.json
.
Example Application using Function Components
function ID() {
return '_' + Math.random().toString(36).substr(2, 9);
}
class Item {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
const baseUrl = 'http://localhost:3000';
class ItemAPI {
url = `${baseUrl}/items`;
constructor() {}
getAll(page = 1, limit = 100) {
return fetch(`${this.url}?_page=${page}&_limit=${limit}`)
.then(this.checkStatus)
.then(this.parseJSON);
}
add(item) {
return fetch(`${this.url}`, {
method: 'POST',
body: JSON.stringify(item),
headers: {
'Content-Type': 'application/json',
},
})
.then(this.checkStatus)
.then(this.parseJSON);
}
update(item) {
return fetch(`${this.url}/${item.id}`, {
method: 'PUT',
body: JSON.stringify(item),
headers: {
'Content-Type': 'application/json',
},
})
.then(this.checkStatus)
.then(this.parseJSON);
}
delete(id) {
return fetch(`${this.url}/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
.then(this.checkStatus)
.then(this.parseJSON);
}
static translateStatusToErrorMessage(status) {
switch (status) {
case 401:
return 'Please login again.';
case 403:
return 'You do not have permission to view the items.';
default:
return 'There was an error retrieving the items. Please try again.';
}
}
//pass translate in to make this more flexible
checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
const httpErrorInfo = {
status: response.status,
statusText: response.statusText,
url: response.url,
};
console.log(
`logging http details for debugging: ${JSON.stringify(httpErrorInfo)}`
);
let errorMessage = ItemAPI.translateStatusToErrorMessage(
httpErrorInfo.status
);
throw new Error(errorMessage);
}
}
parseJSON(response) {
return response.json();
}
}
// REDUX -------------------
//action types
const LOAD_ITEMS_REQUEST = 'LOAD_ITEMS_REQUEST';
const LOAD_ITEMS_SUCCESS = 'LOAD_ITEMS_SUCCESS';
const LOAD_ITEMS_FAILURE = 'LOAD_ITEMS_FAILURE';
const ADD_ITEM_REQUEST = 'ADD_ITEM_REQUEST';
const ADD_ITEM_SUCCESS = 'ADD_ITEM_SUCCESS';
const ADD_ITEM_FAILURE = 'ADD_ITEM_FAILURE';
const UPDATE_ITEM_REQUEST = 'UPDATE_ITEM_REQUEST';
const UPDATE_ITEM_SUCCESS = 'UPDATE_ITEM_SUCCESS';
const UPDATE_ITEM_FAILURE = 'UPDATE_ITEM_FAILURE';
const DELETE_ITEM_REQUEST = 'DELETE_ITEM_REQUEST';
const DELETE_ITEM_SUCCESS = 'DELETE_ITEM_SUCCESS';
const DELETE_ITEM_FAILURE = 'DELETE_ITEM_FAILURE';
//state (initial)
const initialState = {
items: [],
loading: false,
error: null,
};
//reducer
function reducer(state = initialState, action) {
switch (action.type) {
case LOAD_ITEMS_REQUEST:
return { ...state, loading: true };
case LOAD_ITEMS_SUCCESS:
return { ...state, loading: false, items: action.payload };
case LOAD_ITEMS_FAILURE:
return { ...state, loading: false, error: action.payload.message };
case ADD_ITEM_REQUEST:
return { ...state };
case ADD_ITEM_SUCCESS:
return {
...state,
items: [...state.items, action.payload],
};
case ADD_ITEM_FAILURE:
return { ...state, loading: false, error: action.payload.message };
case UPDATE_ITEM_REQUEST:
return { ...state };
case UPDATE_ITEM_SUCCESS:
return {
...state,
items: state.items.map((item) => {
return item.id === action.payload.id
? Object.assign({}, item, action.payload)
: item;
}),
};
case UPDATE_ITEM_FAILURE:
return { ...state, error: action.payload.message };
case DELETE_ITEM_REQUEST:
return { ...state };
case DELETE_ITEM_SUCCESS:
return {
...state,
items: state.items.filter((item) => item.id !== action.payload.id),
};
case DELETE_ITEM_FAILURE:
return { ...state, error: action.payload.message };
default:
return state;
}
}
//action creators
function loadItems() {
return (dispatch) => {
let itemAPI = new ItemAPI();
dispatch({ type: LOAD_ITEMS_REQUEST });
return itemAPI
.getAll(1)
.then((data) => {
dispatch({ type: LOAD_ITEMS_SUCCESS, payload: data });
})
.catch((error) => {
dispatch({ type: LOAD_ITEMS_FAILURE, payload: error });
});
};
}
function addItem(item) {
return (dispatch) => {
let itemAPI = new ItemAPI();
dispatch({ type: ADD_ITEM_REQUEST });
return itemAPI
.add(item)
.then((data) => {
dispatch({ type: ADD_ITEM_SUCCESS, payload: data });
})
.catch((error) => {
dispatch({ type: ADD_ITEM_FAILURE, payload: error });
});
};
}
function updateItem(item) {
return (dispatch) => {
let itemAPI = new ItemAPI();
dispatch({ type: UPDATE_ITEM_REQUEST });
return itemAPI
.update(item)
.then((data) => {
dispatch({ type: UPDATE_ITEM_SUCCESS, payload: data });
})
.catch((error) => {
dispatch({ type: UPDATE_ITEM_FAILURE, payload: error });
});
};
}
function deleteItem(item) {
return (dispatch) => {
let itemAPI = new ItemAPI();
dispatch({ type: DELETE_ITEM_REQUEST });
return itemAPI
.delete(item.id)
.then((data) => {
dispatch({ type: DELETE_ITEM_SUCCESS, payload: item });
})
.catch((error) => {
dispatch({ type: DELETE_ITEM_FAILURE, payload: error });
});
};
}
//store
var ReduxThunk = window.ReduxThunk;
const compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || Redux.compose;
const store = Redux.createStore(
reducer,
compose(Redux.applyMiddleware(...[ReduxThunk]))
);
// UI ---------------------------------
function List(props) {
const { items, loading, error } = props;
const [editingItem, setEditingItem] = React.useState(null);
const dispatch = ReactRedux.useDispatch();
const handleEditClick = (item) => {
setEditingItem(item);
};
const handleCancel = () => {
setEditingItem(null);
};
if (loading) {
return <div>Loading...</div>;
} else if (error) {
return <div>{error}</div>;
} else {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item === editingItem ? (
<Form item={item} onCancel={handleCancel} />
) : (
<p>
<span>{item.name}</span>
<button onClick={() => handleEditClick(item)}>Edit</button>
<button onClick={() => dispatch(deleteItem(item))}>
Remove
</button>
</p>
)}
</li>
))}
</ul>
);
}
}
function Form({ item, onCancel, buttonValue }) {
const [inputValue, setInputValue] = React.useState(item.name || '');
const dispatch = ReactRedux.useDispatch();
const handleChange = (event) => {
event.preventDefault();
setInputValue(event.target.value);
};
const handleFormSubmit = (event) => {
event.preventDefault();
const isNew = !item.id;
const submittedItem = {
id: item ? item.id : ID(),
name: inputValue,
};
if (isNew) {
dispatch(addItem(submittedItem));
} else {
dispatch(updateItem(submittedItem));
}
setInputValue('');
};
const handleCancel = (event) => {
event.preventDefault();
onCancel();
};
return (
<form onSubmit={handleFormSubmit}>
<input value={inputValue} onChange={handleChange} />
<button>{buttonValue || 'Save'}</button>
{onCancel && (
<a href="#" onClick={handleCancel}>
cancel
</a>
)}
</form>
);
}
function Container() {
const items = ReactRedux.useSelector((state) => state.items);
const loading = ReactRedux.useSelector((state) => state.loading);
const error = ReactRedux.useSelector((state) => state.error);
const dispatch = ReactRedux.useDispatch();
React.useEffect(() => {
dispatch(loadItems());
}, [dispatch]);
return (
<div>
<Form item="" buttonValue="Add" />
<List loading={loading} error={error} items={items} />
</div>
);
}
function App() {
return (
<div>
<ReactRedux.Provider store={store}>
<Container />
</ReactRedux.Provider>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
Example Application using Class Components
// API ----------
function ID() {
// Math.random should be unique because of its seeding algorithm.
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
// after the decimal.
return '_' + Math.random().toString(36).substr(2, 9);
}
class Item {
constructor(id, name) {
this.id = id;
this.name = name;
}
}
const baseUrl = 'http://localhost:3000';
class ItemAPI {
url = `${baseUrl}/items`;
constructor() {}
getAll(page = 1, limit = 100) {
return fetch(`${this.url}?_page=${page}&_limit=${limit}`)
.then(this.checkStatus)
.then(this.parseJSON);
}
add(item) {
return fetch(`${this.url}`, {
method: 'POST',
body: JSON.stringify(item),
headers: {
'Content-Type': 'application/json',
},
})
.then(this.checkStatus)
.then(this.parseJSON);
}
update(item) {
return fetch(`${this.url}/${item.id}`, {
method: 'PUT',
body: JSON.stringify(item),
headers: {
'Content-Type': 'application/json',
},
})
.then(this.checkStatus)
.then(this.parseJSON);
}
delete(id) {
return fetch(`${this.url}/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
.then(this.checkStatus)
.then(this.parseJSON);
}
static translateStatusToErrorMessage(status) {
switch (status) {
case 401:
return 'Please login again.';
case 403:
return 'You do not have permission to view the items.';
default:
return 'There was an error retrieving the items. Please try again.';
}
}
//pass translate in to make this more flexible
checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
const httpErrorInfo = {
status: response.status,
statusText: response.statusText,
url: response.url,
};
console.log(
`logging http details for debugging: ${JSON.stringify(httpErrorInfo)}`
);
let errorMessage = ItemAPI.translateStatusToErrorMessage(
httpErrorInfo.status
);
throw new Error(errorMessage);
}
}
parseJSON(response) {
return response.json();
}
}
// REDUX -------------------
//action types
const LOAD_ITEMS_REQUEST = 'LOAD_ITEMS_REQUEST';
const LOAD_ITEMS_SUCCESS = 'LOAD_ITEMS_SUCCESS';
const LOAD_ITEMS_FAILURE = 'LOAD_ITEMS_FAILURE';
const ADD_ITEM_REQUEST = 'ADD_ITEM_REQUEST';
const ADD_ITEM_SUCCESS = 'ADD_ITEM_SUCCESS';
const ADD_ITEM_FAILURE = 'ADD_ITEM_FAILURE';
const UPDATE_ITEM_REQUEST = 'UPDATE_ITEM_REQUEST';
const UPDATE_ITEM_SUCCESS = 'UPDATE_ITEM_SUCCESS';
const UPDATE_ITEM_FAILURE = 'UPDATE_ITEM_FAILURE';
const DELETE_ITEM_REQUEST = 'DELETE_ITEM_REQUEST';
const DELETE_ITEM_SUCCESS = 'DELETE_ITEM_SUCCESS';
const DELETE_ITEM_FAILURE = 'DELETE_ITEM_FAILURE';
//state (initial)
const initialState = {
items: [],
loading: false,
error: null,
};
//reducer
function reducer(state = initialState, action) {
switch (action.type) {
case LOAD_ITEMS_REQUEST:
return { ...state, loading: true };
case LOAD_ITEMS_SUCCESS:
return { ...state, loading: false, items: action.payload };
case LOAD_ITEMS_FAILURE:
return { ...state, loading: false, error: action.payload.message };
case ADD_ITEM_REQUEST:
return { ...state };
case ADD_ITEM_SUCCESS:
return {
...state,
items: [...state.items, action.payload],
};
case ADD_ITEM_FAILURE:
return { ...state, loading: false, error: action.payload.message };
case UPDATE_ITEM_REQUEST:
return { ...state };
case UPDATE_ITEM_SUCCESS:
return {
...state,
items: state.items.map((item) => {
return item.id === action.payload.id
? Object.assign({}, item, action.payload)
: item;
}),
};
case UPDATE_ITEM_FAILURE:
return { ...state, error: action.payload.message };
case DELETE_ITEM_REQUEST:
return { ...state };
case DELETE_ITEM_SUCCESS:
return {
...state,
items: state.items.filter((item) => item.id !== action.payload.id),
};
case DELETE_ITEM_FAILURE:
return { ...state, error: action.payload.message };
default:
return state;
}
}
//action creators
function loadItems() {
return (dispatch) => {
let itemAPI = new ItemAPI();
dispatch({ type: LOAD_ITEMS_REQUEST });
return itemAPI
.getAll(1)
.then((data) => {
dispatch({ type: LOAD_ITEMS_SUCCESS, payload: data });
})
.catch((error) => {
dispatch({ type: LOAD_ITEMS_FAILURE, payload: error });
});
};
}
function addItem(item) {
return (dispatch) => {
let itemAPI = new ItemAPI();
dispatch({ type: ADD_ITEM_REQUEST });
return itemAPI
.add(item)
.then((data) => {
dispatch({ type: ADD_ITEM_SUCCESS, payload: data });
})
.catch((error) => {
dispatch({ type: ADD_ITEM_FAILURE, payload: error });
});
};
}
function updateItem(item) {
return (dispatch) => {
let itemAPI = new ItemAPI();
dispatch({ type: UPDATE_ITEM_REQUEST });
return itemAPI
.update(item)
.then((data) => {
dispatch({ type: UPDATE_ITEM_SUCCESS, payload: data });
})
.catch((error) => {
dispatch({ type: UPDATE_ITEM_FAILURE, payload: error });
});
};
}
function deleteItem(item) {
return (dispatch) => {
let itemAPI = new ItemAPI();
dispatch({ type: DELETE_ITEM_REQUEST });
return itemAPI
.delete(item.id)
.then((data) => {
dispatch({ type: DELETE_ITEM_SUCCESS, payload: item });
})
.catch((error) => {
dispatch({ type: DELETE_ITEM_FAILURE, payload: error });
});
};
}
//store
var ReduxThunk = window.ReduxThunk;
const compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || Redux.compose;
const store = Redux.createStore(
reducer,
compose(Redux.applyMiddleware(...[ReduxThunk]))
);
// UI ---------------------------------
class List extends React.Component {
state = {
editingItem: null,
};
handleEditClick = (item) => {
this.setState({ editingItem: item });
};
handleCancel = (item) => {
this.setState({ editingItem: null });
};
render() {
const { items, onRemove, onUpdate, loading, error } = this.props;
if (loading) {
return <div>Loading...</div>;
} else if (error) {
return <div>{error}</div>;
} else {
return (
<ul>
{items.map((item) => (
<li key={item.id}>
{item === this.state.editingItem ? (
<Form
item={item}
onSubmit={onUpdate}
onCancel={this.handleCancel}
/>
) : (
<p>
<span>{item.name}</span>
<button onClick={() => this.handleEditClick(item)}>
Edit
</button>
<button onClick={() => onRemove(item)}>Remove</button>
</p>
)}
</li>
))}
</ul>
);
}
}
}
class Form extends React.Component {
state = {
inputValue: this.props.item.name || '',
};
handleChange = (event) => {
event.preventDefault();
this.setState({ inputValue: event.target.value });
};
handleFormSubmit = (event) => {
event.preventDefault();
const item = {
id: this.props.item ? this.props.item.id : ID(),
name: this.state.inputValue,
};
this.props.onSubmit(item);
this.setState({ inputValue: '' });
};
handleCancel = (event) => {
event.preventDefault();
this.props.onCancel();
};
render() {
return (
<form onSubmit={this.handleFormSubmit}>
<input value={this.state.inputValue} onChange={this.handleChange} />
<button>{this.props.buttonValue || 'Save'}</button>
{this.props.onCancel && (
<a href="#" onClick={this.handleCancel}>
cancel
</a>
)}
</form>
);
}
}
class Container extends React.Component {
componentDidMount() {
this.props.onLoad();
}
render() {
return (
<div>
<Form item="" onSubmit={this.props.onAdd} buttonValue="Add" />
<List {...this.props} />
</div>
);
}
}
// React Redux (connect)---------------
function mapStateToProps(state) {
return {
items: state.items,
loading: state.loading,
error: state.error,
};
}
// // Same thing, just with lots of ES6 shorthand
// const mapState = ({ items, loading, error }) => ({
// items,
// loading,
// error
// });
const mapDispatchToProps = {
onLoad: loadItems,
onAdd: addItem,
onUpdate: updateItem,
onRemove: deleteItem,
};
const ConnectedContainer = ReactRedux.connect(
mapStateToProps,
mapDispatchToProps
)(Container);
// App
class App extends React.Component {
render() {
return (
<div>
<ReactRedux.Provider store={store}>
<ConnectedContainer />
</ReactRedux.Provider>
</div>
);
}
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
Notes
- When dealing with this much code in one file make sure things are defined before they are used.
- For example,
List
would be undefined if you tried to connect it before it was defined further down inmain.js
.