Persisting Redux state to local storage - arrays

I've created a simple to do app using React. I've attempted to persist state using local storage. However, the local storage code I've added is somehow preventing my components from rendering altogether. Not only are the todos saved in state not appearing, none of my components will render. I get a blank page on refresh. Can someone help me figure out what's wrong?
Here's what happens on the initial save after the local storage code is included. It loads the components just fine, but the to dos that are already in state are not shown:
After using the form to add to dos and refreshing the page, this happens. None of the components are shown whatsoever. Just a blank page.
Here is the local storage code inside my index.js file. I'm pretty sure the problem is here but I have included the code for the other components and the reducer as well:
const persistedState = localStorage.getItem('state') ? JSON.parse(localStorage.getItem('state')) : [];
const store = createStore(reducer, persistedState);
store.subscribe(() => {
localStorage.setItem('state', JSON.stringify(store.getState()));
})
The index.js file in its entirety:
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from "redux";
import { Provider } from "react-redux";
import './index.css';
import App from './App';
import { reducer } from "./reducers/todoReducer";
import * as serviceWorker from './serviceWorker';
const persistedState = localStorage.getItem('state') ? JSON.parse(localStorage.getItem('state')) : [];
const store = createStore(reducer, persistedState);
store.subscribe(() => {
localStorage.setItem('state', JSON.stringify(store.getState()));
})
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
the other components:
TodoList.js:
import Todo from "./Todo";
const TodoList = props => {
return (
<ul className="task-list">
{props.state.map(task => (
<Todo task={task} />
))}
</ul>
)
}
const mapStateToProps = state => {
return {
state: state
}
}
export default connect(mapStateToProps)(TodoList);
TodoForm.js:
const TodoForm = props => {
const [newItemText, setNewItemText] = useState("");
const handleChanges = e => {
e.preventDefault();
setNewItemText(e.target.value);
};
const saveState = () => localStorage.setItem("props.state", JSON.stringify(props.state));
useEffect(() => {
const todos = localStorage.getItem('state');
if (todos) props.setState({ [props.state]: JSON.parse(props.state) })
}, [])
return (
<div className="form-div">
<input
className="add-input"
name="todo"
type="text"
placeholder="enter a task"
value={newItemText}
onChange={handleChanges}
/>
<button
className="add-button"
onClick = {e => {
e.preventDefault();
props.addItem(newItemText);
saveState();
}}>Add a Task
</button>
<button
className="add-button"
onClick={e => {
e.preventDefault();
props.removeCompleted();
}}>Remove Completed
</button>
</div>
)
}
const mapStateToProps = state => {
return {
state: state
}
}
export default connect(mapStateToProps, {addItem, removeCompleted})(TodoForm);
Todo.js:
const Todo = props => {
return (
<li
className="tasks"
style={{textDecoration: props.task.completed ? 'line-through' : 'none'}}
onClick={() => props.toggleCompleted(props.task.id)}>
{props.task.item}
</li>
)
}
const mapStateToProps = state => {
return {
state: state
}
}
export default connect(mapStateToProps, {toggleCompleted})(Todo);
todoReducer.js:
export const initialState = [
{ item: 'Learn about reducers', completed: false, id: 1 },
{ item: 'review material from last week', completed: false, id: 2 },
{ item: 'complete reducer todo project', completed: false, id: 3 }
]
export const reducer = (state = initialState, action) => {
switch(action.type) {
case ADD_ITEM:
// console.log(action.payload)
return [
...state,
{
item: action.payload,
completed: false,
id: Date.now()
}
]
case TOGGLE_COMPLETED:
const toggledState = [...state];
toggledState.map(item => {
if(item.id === action.payload) {
item.completed = !item.completed;
}
})
console.log(toggledState);
state = toggledState;
return state;
case REMOVE_COMPLETED:
return state.filter(item => !item.completed);
default:
return state;
}
}
export default reducer;
App.js:
import React from 'react';
import './App.css';
// components
import TodoList from "./components/TodoList";
import TodoForm from "./components/TodoForm";
function App() {
return (
<div className="App">
<h1 className="title">To Do List</h1>
<TodoList />
<TodoForm />
</div>
);
}
export default App;
actions.js:
export const ADD_ITEM = 'ADD_ITEM';
export const TOGGLE_COMPLETED = 'TOGGLE_COMPLETED';
export const REMOVE_COMPLETED = 'REMOVE_COMPLETED';
export const addItem = input => {
return {
type: ADD_ITEM, payload: input
}
};
export const toggleCompleted = (id) => {
return {
type: TOGGLE_COMPLETED, payload: id
}
};
export const removeCompleted = () => {
return {
type: REMOVE_COMPLETED
}
};

Related

How to rerender when Redux state is changed

Hi I have 2 components.
The first component provides a read (useSelector) from the Redux state object and renders its contents
The second component ensures the insertion of new data into the same Redux state object
How to achieve that when a Redux state object changes with the help of the second component, the first component captures this change and renders the new content of the object again.
I tried to add in the component element:
useEffect(() => {
...some actions
}, [reduxStateObject]);
But it gives me too many requests.
/// EDIT add real example
component
import React from "react";
import { useSelector } from "react-redux";
const ToDoList = () => {
const { todos } = useSelector((state) => state.global);
return (
<div>
<h1>Active</h1>
{todos
?.filter((todo) => !todo.isCompleted)
.sort((a, b) => (a.deadline < b.deadline ? 1 : -1))
.map((todo, id) => {
const date = new Date(todo.deadline).toLocaleString();
return (
<div key={id}>
<p>{todo.text}</p>
<p>{date}</p>
</div>
);
})}
</div>
);
};
export default ToDoList;
component
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getToDoItems } from "../redux/globalSlice";
import ToDoList from "../components/ToDoList";
const ToDoWall = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getToDoItems(1));
}, [dispatch]);
const submitForm = (e) => {
dispatch(postToDoItem(e.data));
};
return (
<>
<ToDoList />
<form onSubmit={submitForm}>
<input type="text"></input>
<input type="submit" value="" />
</form>
</>
);
};
export default ToDoWall;
/// EDIT add Reducer
import { createSlice } from "#reduxjs/toolkit";
import axios from "axios";
const initialState = {
todos: null,
};
export const globalSlice = createSlice({
name: "global",
initialState,
reducers: {
setItems: (state, action) => {
state.todos = action.payload;
},
},
});
export const { setItems } = globalSlice.actions;
export default globalSlice.reducer;
// Load todo items
export const getToDoItems = (id) => {
return (dispatch) => {
axios
.get(`https://xxx.mockapi.io/api/list/${id}/todos`)
.then((resp) => dispatch(setItems(resp.data)));
};
};
// Post a list name
export const postNameList = (data) => {
return (dispatch) => {
axios.post("https://xxx.mockapi.io/api/list", {
name: data,
});
};
};
// Post a todo item
export const postToDoItem = (id, data) => {
return (dispatch) => {
axios.post(
`https://xxx.mockapi.io/api/list/${id}/todos`,
{
listId: id,
title: data.title,
text: data.text,
deadline: +new Date(data.deadline),
isCompleted: false,
}
);
};
};
As far as I understood, you don't need to do anything. When you dispatch action to change state in redux store, it'll change, and all components that use that state will get it, you don't need to worry about updating anything.

TypeError: todos.map is not a function

I am building a todo app with React, Redux and TypeScript with hooks.
I cannot figure out why it succesfully compiles but fails to run in the browser.
Error:
TypeError: todos.map is not a function.
I have checked the type of todo which is of type Todo[] (i.e. an array) and, correspondingly, it can iterate using map.
I am not sure how to fix this.
Code here: https://codesandbox.io/s/agitated-meadow-53w0u?file=/src/App.tsx
App.tsx
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { useTypedSelector } from "./index";
function AddToDo() {
const [input, setInput] = useState("");
const dispatch = useDispatch();
function handleInput(e: React.ChangeEvent<HTMLInputElement>) {
setInput(e.target.value);
}
//dispatch to store
function handleAddTodo() {
dispatch({ text: input })
setInput("");
}
return (
<div>
<input type="text" onChange={e => handleInput(e)} value={input} />
<button type="button" onClick={handleAddTodo}>
Add todo
</button>
</div>
);
}
//TodoList
export interface Todo {
text: string;
}
function TodoList() {
const todos = useTypedSelector((state) => state)
return (
<ul className="todo-list">
{todos.map((todo: Todo) => {
return <Todo todo={todo} />;
})}
</ul>
);
}
//Todo
function Todo({ todo }: { todo: Todo }) {
return <li>{todo.text}</li>;
}
function App() {
return (
<div className="App">
<AddToDo />
<TodoList />
</div>
);
}
export default App;
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider, TypedUseSelectorHook, useSelector } from 'react-redux'
import { createStore, combineReducers } from 'redux'
import App, { Todo } from './App';
//actions
const ADD_TODO = 'ADD_TODO'
type Action = AddTodo
export function addTodo(text: string) {
return {
type: ADD_TODO,
text
}
}
type AddTodo = ReturnType<typeof addTodo>
const INITIAL_STATE = [] as Todo[]
//reducer
function todoReducer(state = INITIAL_STATE, action: Action): Todo[] {
switch (action.type) {
case ADD_TODO:
const todos: Todo[] = [
...state,
{
text: action.text,
}
]
return todos
default:
return state
}
}
const todoApp = combineReducers({
todos: todoReducer
}
)
type RootState = ReturnType<typeof todoReducer>
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector
//store
const store = createStore(todoApp)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
As per #JohnHass
because RootState isn't the actual type of the root state, you've made
it Todo[] when the state is actually { todos: Todo[] }
Then you need to change is from this
const todos = useTypedSelector(state => state);
To :
const todos = useTypedSelector(state => state.todos);
state was not returning array but object with todos key,
Another issue :
Not passing event type :
function handleAddTodo() {
dispatch({ text: input }); // <--- Not even passing action type
setInput("");
}
Change it to :
function handleAddTodo() {
dispatch({ type: "ADD_TODO", text: input });
setInput("");
}
//-------------- OR ---------------
import { useTypedSelector , addTodo } from "./index";
//dispatch to store
function handleAddTodo() {
dispatch(addTodo(input));
setInput("");
}
Issue
Your state.todos is an Array, but you are assigning const todos to the entire state, that is an object { todos: Todo[], ... }.
Possible solutions
1. Get just todos, requesting just that property inside the callback:
const todos = useTypedSelector((state) => state.todos)
2. Deconstruct the assignment:
const { todos } = useTypedSelector((state) => state)

React Context state not shared between components

I have been using Redux for a long time, but now decided to try out the new ContextAPI.
I got it working with one component/page (using NextJs), however the state isn't shared between pages/components.
store.js
import React, { createContext, useReducer } from 'react';
import reducer from './reducer'
const initialState = {
players: [],
};
const Store = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>
{children}
</Context.Provider>
)
};
export const Context = createContext(initialState);
export default Store;
reducer.js
const Reducer = (state, action) => {
switch (action.type) {
case 'ADD_PLAYER':
return {
...state,
players: [...state.players, action.payload],
};
case 'REMOVE_PLAYER_BY_INDEX':
const array = state.players;
if (array) {
array.splice(action.payload, 1);
}
return {
...state,
players: !array ? [] : array,
};
default:
return state;
}
};
export default Reducer;
add players page /players/add (addplayerspage.js)
import React, { useContext } from 'react';
import map from 'lodash/map';
import isEqual from 'lodash/isEqual';
import { Context } from '../../../context';
const PlayerCreatePage = () => {
const [_, dispatch] = useContext(Context);
const handleAddPlayer = () => {
dispatch({ type: 'ADD_PLAYER', payload: Math.random() });
};
const handleRemovePlayerByIndex = (index) => {
dispatch({ type: 'REMOVE_PLAYER_BY_INDEX', payload: index });
};
return (
<div className="layout">
<div>
<Context.Consumer>
{([state]) => {
const { players } = state;
return map(players, (p, i) => <div
key={i}
onClick={() => handleRemovePlayerByIndex(i)}
>
{p}
</div>
)
}}
</Context.Consumer>
</div>
<button onClick={() => handleAddPlayer()}>Add new</button>
</div>
);
};
export default React.memo(PlayerCreatePage, (prev, next) => isEqual(prev, next));
lobby players page /players/lobby (lobbyplayerspage.js)
import React, { useContext } from 'react';
import map from 'lodash/map';
import { Context } from '../../../context';
const PlayersLobbyPage = () => {
const [state, _] = useContext(Context);
return (
<div>
<div>
{map(state.players, (p, i) => <div
key={i}
>
{p}
</div>
)}
</div>
</div>
);
};
export default PlayersLobbyPage;
_app.js (NextJs)
import App, { Container } from 'next/app';
import '../styles/main.css';
import Store from '../context';
class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<Container>
<Store>
<Component {...pageProps} />
</Store>
</Container>
);
}
}
export default MyApp;
THE PROBLEM:
Have two tabs open
Add players
Lobby
Add a new player
See that player is added on 'Add players' page
2.See that NOTHING happens on 'Lobby' page
Okay, so the issue is that I was trying to "share" context api state between different open tabs, it doesn't work like that by default, even for redux (tried adding it and faced same result), for redux there's a redux-state-sync library for that, nonetheless I will use sockets in future, so this won't be an issue.
Closed.

How to pass object to Redux

I would like ask you about passing object to Redux.
Below is my code.
// src/actions/writingType.js
export const write = () => ({
type: 'WRITE',
})
export const update = (obj) => ({
type: 'UPDATE',
obj
})
// src/reducers/writingType.js
const initialState = {
writingType: "WRITE",
obj: null
}
const writingTypeReducer = (state = initialState, action) => {
console.log('\n inside writingTypeReducer');
console.log(action);
switch (action.type) {
case 'WRITE':
return {
...state,
writingType: 'WRITE'
};
case 'UPDATE':
return {
...state,
writingType: 'UPDATE',
obj: action.obj
};
default:
return state;
}
}
export default writingTypeReducer;
// Contentview.js
import React, { useContext } from 'react';
import { Route, Link } from 'react-router-dom';
import MarkdownRenderer from 'react-markdown-renderer';
import './Contentview.css';
import { connect } from 'react-redux'
import { write, update } from '../../actions/writingType'
import { UserConsumer } from '../../contexts/userContext';
import { Test } from '../../contexts/Test';
const Contentview = (props) => {
/*
category: "React"
contentObj:
contents: "something"
createdDatetime: "2019.10.26 08:52:05"
title: "something"
wikiIndex: 1
*/
console.log('\n Contentview');
console.log(props);
console.log('\n update(props.contentObj);');
update(props.contentObj);
const url = "/editor/" + props.category;
const updateUrl = "/update/" + props.category;
return (
<div>
<div className="categoryDiv">{props.category}</div>
<div className="writingDiv"><Link to={url}> A </Link></div>
<div className="updateDiv"><Link to={updateUrl} > B </Link></div>
<hr />
<MarkdownRenderer markdown={props.contentObj.contents} />
</div>
);
};
// export default Contentview;
const mapStateToProps = (state, props) => ({
writetypestate: state.writingType,
obj: props.contentObj
})
const mapDispatchToProps = dispatch => ({
write: () => dispatch(write()),
update: (obj) => {
console.log('Contentview, mapDispatchToProps, update');
dispatch(update(obj))
}
})
export default connect(mapStateToProps, mapDispatchToProps)(Contentview)
I used update(props.contentObj); in Contentview.js to pass props.contentObj to Redux and update obj of initialState in src/reducers/writingType.js. But obj of initialState hasn't changed and existed as null.
How should I change code?
Thank you.
use props.update to call in the main file
// Contentview.js
import React, { useContext } from 'react';
import { Route, Link } from 'react-router-dom';
import MarkdownRenderer from 'react-markdown-renderer';
import './Contentview.css';
import { connect } from 'react-redux'
import { write, update } from '../../actions/writingType'
import { UserConsumer } from '../../contexts/userContext';
import { Test } from '../../contexts/Test';
const Contentview = (props) => {
/*
category: "React"
contentObj:
contents: "something"
createdDatetime: "2019.10.26 08:52:05"
title: "something"
wikiIndex: 1
*/
console.log('\n Contentview');
console.log(props);
console.log('\n update(props.contentObj);');
props.update(props.contentObj);
const url = "/editor/" + props.category;
const updateUrl = "/update/" + props.category;
return (
<div>
<div className="categoryDiv">{props.category}</div>
<div className="writingDiv"><Link to={url}> A </Link></div>
<div className="updateDiv"><Link to={updateUrl} > B </Link></div>
<hr />
<MarkdownRenderer markdown={props.contentObj.contents} />
</div>
);
};
// export default Contentview;
const mapStateToProps = (state, props) => ({
writetypestate: state.writingType,
obj: props.contentObj
})
const mapDispatchToProps = dispatch => ({
write: () => dispatch(write()),
update: (obj) => {
console.log('Contentview, mapDispatchToProps, update');
dispatch(update(obj))
}
})
export default connect(mapStateToProps, mapDispatchToProps)(Contentview)
Please use the above code

Redux action not firing onclick

Doing a redux todo to learn and having trouble on the toggle todo.
When i trigger the onclick i get an error in the console.
"Failed prop type: The prop items is marked as required in ItemsList, but its value is undefined."
and
"Cannot read property 'length' of undefined" on items.length
I've consoled logged the action and it seems to be returning the state. not sure what i'm doing wrong. code is below.
actions
export const addItem = content => {
return { type: ADD_ITEM, content };
};
export const toggleTodo = (id) => {
return {
type: TOGGLE_TODO,
id,
};
};
my initial state is:
import { ADD_ITEM, TOGGLE_TODO } from './constants';
let nextId = 4;
export const initialState = {
items: [
{ id: 1, content: 'Call mum', completed: false},
{ id: 2, content: 'Buy cat food', completed: true },
{ id: 3, content: 'Water the plants', completed: false },
],
};
const reducer = (state = initialState, action) => {
console.log("action", action.type);
switch (action.type) {
case ADD_ITEM:
const newItem = {
id: nextId++,
content: action.content,
completed: false
};
return {
...state,
items: [...state.items, newItem],
};
case TOGGLE_TODO:
return state.items.map(todo => {
console.log("state", state);
console.log("state.items", state.items);
console.log("todo",todo);
if (todo.id !== action.id) {
return state;
}
return {
...state,
completed: !todo.completed,
};
});
default:
return state;
}
};
export default reducer;
and my list component is
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Todo from "../ItemTodo/index"
import { toggleTodo } from "../../logic/actions";
import './styles.css';
export const ItemsList = ({ items, onTodoClick }) => {
return (
<div>
<ul className={'itemsList-ul'}>
{items.length < 1 && <p id={'items-missing'}>Add some tasks above.</p>}
{items.map(item =>
<Todo
key={item.id}
{...item}
onClick={() => onTodoClick(item.id)}
/>
)}
</ul>
</div>
);
};
ItemsList.propTypes = {
items: PropTypes.array.isRequired,
onTodoClick: PropTypes.func.isRequired,
};
const mapStateToProps = state => {
return { items: state.todos.items };
};
const mapDispatchToProps = dispatch => ({
onTodoClick: id => dispatch(toggleTodo(id))
});
export default connect(mapStateToProps, mapDispatchToProps)(ItemsList);
and my todo component is
import React from 'react';
import PropTypes from 'prop-types';
const Todo = ({onClick, completed, content }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none',
}}
>
{content}
</li>
);
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
content: PropTypes.string.isRequired,
};
export default Todo;
Create store
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import configureStore from './redux/store';
import Header from './components/Header';
import ItemCreator from './components/ItemCreator';
import ItemsList from './components/ItemsList';
import './app.css';
const store = configureStore();
class App extends Component {
render() {
return (
<Provider store={store}>
<div className="app">
<Header />
<div>
<ItemCreator />
<ItemsList />
</div>
</div>
</Provider>
);
}
}
export default App;
import { createStore, applyMiddleware, compose } from 'redux';
import createReducer from './reducers';
const composeEnhancers =
(process.env.NODE_ENV !== 'production' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
const configureStore = (initialState = {}) => {
return createStore(
createReducer(),
initialState,
composeEnhancers(applyMiddleware())
);
};
export default configureStore;
import { combineReducers } from 'redux';
import reducer from '../logic/reducer';
export default function createReducer() {
return combineReducers({
todos: reducer,
});
}
I haven't tested it, but I think this part is incorrect TOGGLE_TODO:
return state.items.map(todo => {
console.log("state", state);
console.log("state.items", state.items);
console.log("todo",todo);
if (todo.id !== action.id) {
return state;
}
return {
...state,
completed: !todo.completed,
};
});
it should be:
return {
...state,
items: state.items.map((todo) => {
if (todo.id === action.id) {
return { ...todo, completed: !todo.completed };
}
return todo;
})
};

Resources