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
updateActiveName
andupdateActiveIngredients
function because there are only these two fields that get added by the form. Would it be better instead to use anupdateActiveObject
and 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
mapDispatchToProps
are usually the same. For exampleUPDATE_ACTIVE_INGREDIENTS
,updateActiveIngredients
action creator andupdateActiveIngredients
function. Is there a better approach to this?In the
addRecipe
function insidemapDispatchToProps
, I am validating thename
andingredients
values. Should this go inside this function, or somewhere else?In the
addRecipe
function insidemapDispatchToProps
, I am dispatching many actions. Is this correct practise?
Any other feedback for improving my Redux is most welcome.