4
\$\begingroup\$

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)}>&times;</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:

  1. I have an updateActiveName and updateActiveIngredients function because there are only these two fields that get added by the form. Would it be better instead to use an updateActiveObject 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.

  2. The naming of my action creators, reducers, and functions inside mapDispatchToProps are usually the same. For example UPDATE_ACTIVE_INGREDIENTS, updateActiveIngredients action creator and updateActiveIngredients function. Is there a better approach to this?

  3. In the addRecipe function inside mapDispatchToProps, I am validating the name and ingredients values. Should this go inside this function, or somewhere else?

  4. In the addRecipe function inside mapDispatchToProps, I am dispatching many actions. Is this correct practise?

Any other feedback for improving my Redux is most welcome.

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

I've only started working with React/Redux myself, but here are my thoughts.

  1. In my system I am saving the form values to local component state until the form is saved. I have one onChange handler method which checks which form field it is and then will setState.

  2. This is something I also find with React/Redux - a lot of boilerplate and repetition to update a few pieces of data... Similar to yourself I have the Action Types / Action Creators / API Calls / Reducers / Component methods all with very similar names. I think that is just the way it is. I'm also looking to include redux-saga in our code so that potentially means even more of the same named methods.

  3. I use parsley for form validating, so that code is handled by the library. If I really needed to do further checking, I would do it in the onChange before changing the local component state.

  4. You have everything in one component at the moment for listing the recipes and adding the recipes. I would split them into separate components and then once you're finished with saving a new recipe, render the list again. Your app is very much like a To Do list app, have a look for examples of that.

If it wasn't for the speed of react-redux, I probably wouldn't be bothered with it!

Here's an example of a to do app that has a similar structure to my own app (which I hope is well structured lol). https://github.com/reactjs/redux/tree/master/examples/todomvc

\$\endgroup\$
1
\$\begingroup\$

The one thing that I would change is that since you are already using react-redux library, you might as well do this -

let store = createStore(recipes, window.__REDUX_DEVTOOLS_EXTENSION__())

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

So that you don't have to manually subscribe like so -

store.subscribe(render)
render()


About the questions that you mentioned -

  1. I have an updateActiveName and updateActiveIngredients function because there are only these two fields that get added by the form. Would it be better instead to use an updateActiveObject 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.

This is again an opinion, but I wouldn't do that. One reason is that they don't/might not have similar types.

An example where this might work is - say - you have a big form where you have to enter a number of dates, one for birthdate, one for date of joining the company etc. You could a function changeDate()/updateDate() which would serve as a common function to update dates.


  1. The naming of my action creators, reducers, and functions inside mapDispatchToProps are usually the same. For example UPDATE_ACTIVE_INGREDIENTS, updateActiveIngredients action creator and updateActiveIngredients function. Is there a better approach to this?

This is fine, so long as you stick with it. You anyways don't have to worry about the ActionCreator names because, if you access the methods/functions that are defined in mapDispatchToProps() and don't directly dispatch() actions in the Component itself, you wouldn't find much problems with understanding what goes where.

What I mean is, if you use - this.props.updateActiveIngredients() - then it is fine. However, if you use - this.props.dispatch(updateActiveIngredients()) you might get confused.


3.In the addRecipe function inside mapDispatchToProps, I am validating the name and ingredients values. Should this go inside this function, or somewhere else?

Technically, it is a function. You can move it to a named function, but for something this small this is alright.


4.In the addRecipe function inside mapDispatchToProps, I am dispatching many actions. Is this correct practice?

This is correct. You can also add a redux-thunk middleware and add an ActionCreator that returns a function instead of a simple JSON object.

For e.g.:

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())
      }
    }
  }
}

could be replaced by something like this -

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    addRecipe: (id, name, ingredients) => dispatch(addRecipe(id, name, ingredients))
  }
}
// Action Creator
addRecipe(id, name, ingredients) => {
  return (dispatch, getState) => {
    if (name && ingredients) {
      dispatch(addRecipe(id, name, ingredients))
      dispatch(updateActiveName(''))
      dispatch(updateActiveIngredients(''))
      // Get the proper state that you want using getState()
      const something = getState()
      saveStoreRecipesToLocalStorage(something)
    }
  }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.