I made a simple React + Redux app which allows you to add and remove recipes to a list.
Here are the user stories the app fulfills:
User Story: I can create recipes that have names and ingredients.
User Story: I can see an index view where the names of all the recipes are visible.
User Story: I can click into any of those recipes to view it.
User Story: I can delete these recipes.
User Story: All new recipes I add are saved in my browser's local storage. If I refresh the page, these recipes will still be there.
Code:
import React from 'react'
import ReactDOM from 'react-dom'
import { connect } from 'react-redux'
import { createStore } from 'redux'
const ADD_RECIPE = 'ADD_RECIPE'
const SHOW_RECIPE = 'SHOW_RECIPE'
const EDIT_RECIPE = 'EDIT_RECIPE'
const UPDATE_ACTIVE_NAME = 'UPDATE_ACTIVE_NAME'
const UPDATE_ACTIVE_INGREDIENTS = 'UPDATE_ACTIVE_INGREDIENTS'
const TOGGLE_ACTIVE_ID = 'TOGGLE_ACTIVE_ID'
const DELETE_RECIPE = 'DELETE_RECIPE'
const initialState = {
activeName: '',
activeIngredients: '',
activeId: '',
recipes: [
{
id: 1,
name: 'Pizza',
ingredients: 'Dough, tomato, cheese, salt, pepper, olives, mushrooms, ham.'
},
{
id: 2,
name: 'Sherpherd\'s Pie',
ingredients: 'Potato, lamb'
},
{
id: 3,
name: 'Huel Shake',
ingredients: 'Milk, huel'
}
]
}
// action creators
const saveStoreRecipesToLocalStorage = (state) => {
localStorage.setItem('state', JSON.stringify(
{...state, activeId: ""}
));
}
const getStoreRecipesFromLocalStorage = () => {
const state = JSON.parse(localStorage.getItem('state'))
return state.recipes.length && state
}
const addRecipe = (id, name, ingredients) => {
return {
type: ADD_RECIPE,
payload: {
id,
name,
ingredients
}
}
}
const updateActiveName = payload => {
return {
type: UPDATE_ACTIVE_NAME,
payload
}
}
const updateActiveIngredients = payload => {
return {
type: UPDATE_ACTIVE_INGREDIENTS,
payload
}
}
const toggleActiveId = id => {
return {
type: TOGGLE_ACTIVE_ID,
payload: id
}
}
const deleteRecipe = id => {
return {
type: DELETE_RECIPE,
payload: id
}
}
// reducer
function recipes (state = getStoreRecipesFromLocalStorage() || initialState, action) {
console.warn('action:', action)
switch (action.type) {
case TOGGLE_ACTIVE_ID:
return {...state, activeId: action.payload}
case UPDATE_ACTIVE_NAME:
return {...state, activeName: action.payload}
case UPDATE_ACTIVE_INGREDIENTS:
return {...state, activeIngredients: action.payload}
case ADD_RECIPE:
return {...state, recipes: [...state.recipes, action.payload]}
case DELETE_RECIPE:
return {...state, recipes: state.recipes.filter(recipe => recipe.id !== action.payload)}
case EDIT_RECIPE:
return
default:
return state
}
}
const Recipes = (props) => {
getStoreRecipesFromLocalStorage()
const {addRecipe, updateActiveIngredients, updateActiveName, toggleActiveId, deleteRecipe, state} = props
const id = new Date().getTime() / 1000
return (
<div style={{width: 320}}>
<form onSubmit={e => {
e.preventDefault()
addRecipe(id, state.activeName, state.activeIngredients)
}}>
<input
type="text"
placeholder="Name"
value={state.activeName}
onChange={(e) => updateActiveName(e.target.value)}
/>
<input
placeholder="Ingredients"
value={state.activeIngredients}
onChange={e => updateActiveIngredients(e.target.value)}
/>
<button type="submit">Add Recipe</button>
</form>
{state.recipes.map(item => <ul key={item.name}>
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<li onClick={() => toggleActiveId(item.id)}>
<div>{item.name}</div>
{state.activeId === item.id && <div>{item.ingredients}</div>}
</li>
<div style={{color: 'red'}} onClick={() => deleteRecipe(item.id)}>×</div>
</div>
</ul>)}
</div>
)
}
const mapStateToProps = (state) => {
return {
state
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
addRecipe: (id, name, ingredients) => {
if (name && ingredients) {
dispatch(addRecipe(id, name, ingredients))
dispatch(updateActiveName(''))
dispatch(updateActiveIngredients(''))
saveStoreRecipesToLocalStorage(ownProps.store.getState())
}
},
updateActiveName: name => dispatch(updateActiveName(name)),
updateActiveIngredients: ingredients => dispatch(updateActiveIngredients(ingredients)),
toggleActiveId: id => dispatch(toggleActiveId(id)),
deleteRecipe: id => {
dispatch(deleteRecipe(id))
saveStoreRecipesToLocalStorage(ownProps.store.getState())
}
}
}
const App = connect(
mapStateToProps,
mapDispatchToProps
)(Recipes)
const store = createStore(recipes, window.__REDUX_DEVTOOLS_EXTENSION__())
const render = () => ReactDOM.render(<App store={store}/>, document.getElementById('root'))
store.subscribe(render)
render()
I would like to structure the app better by splitting this file into separate files but for the sake of this question I have kept it in one file.
Demo (adapted for codepen without localStorage features): https://codepen.io/alanbuchanan/pen/LQWKBZ
Making this small app brought about some questions I have about Redux:
I have an
updateActiveNameandupdateActiveIngredientsfunction because there are only these two fields that get added by the form. Would it be better instead to use anupdateActiveObjectand run this when either field changes? I see this as a benefit for when other fields get added, but less efficient because I will be running an onChange function for several fields that aren't changing.The naming of my action creators, reducers, and functions inside
mapDispatchToPropsare usually the same. For exampleUPDATE_ACTIVE_INGREDIENTS,updateActiveIngredientsaction creator andupdateActiveIngredientsfunction. Is there a better approach to this?In the
addRecipefunction insidemapDispatchToProps, I am validating thenameandingredientsvalues. Should this go inside this function, or somewhere else?In the
addRecipefunction insidemapDispatchToProps, I am dispatching many actions. Is this correct practise?
Any other feedback for improving my Redux is most welcome.