I want to Todo List in use React.js + Redux.
I make reducer file:
import { ADD_POST, REMOVE_POST } from "../actions/index.jsx";
const initialState = {
title: "",
content: ""
};
export default function Post(state = initialState, action) {
switch (action.type) {
case ADD_POST:
return [
...state,
{
id: action.id,
title: action.title,
content: action.content
}
];
case REMOVE_POST:
return state.filter(({ id }) => id !== action.id);
default:
return state;
}
}
And, I edit App.js :
class App extends Component {
render() {
return (
<div className="App">
<Input />
<List posts={this.props.allPosts} />
</div>
);
}
}
const mapStateToProps = state => {
return {
allPosts: [state.title, state.content]
};
};
export default connect(mapStateToProps, null)(App);
And, List Component is...:
render() {
return (
<div>
<ul>
{this.props.posts.map((post, index) => (
<Item {...post} key={index} />
))}
</ul>
</div>
);
}
}
I am experiencing the error "Can not read property 'map' of undefined" and can not proceed.
How can I fix it?
I'm referring to multiple sources, but I'm having difficulty because I can only see text for one 'text' state, and two sources like 'title' and 'content' states.
-------_FIX
I fix error, but props.state is blank.
I add input tag with texts but it not change everything.
-------Actions
export const ADD_POST = "ADD_POST";
export const REMOVE_POST = "REMOVE_POST";
let nextId = 0;
export function addPost(title, content) {
return {
type: ADD_POST,
id: nextId++,
title,
content
};
}
export function removePost(id) {
return {
type: REMOVE_POST,
id
};
}
I think you're confusing with the data type of your state. The below snippet might work for you. I've kept your state as an array of posts with initialState being an empty array.
So in your reducer file, initialise the initialState as:
import {
ADD_POST,
REMOVE_POST
} from "../actions/index.jsx";
const initialState = [];
export default function Post(state = initialState, action) {
switch (action.type) {
case ADD_POST:
return [
...state,
{
id: action.id,
title: action.title,
content: action.content
}
];
case REMOVE_POST:
return state.filter(({
id
}) => id !== action.id);
default:
return state;
}
}
In App.js, in the function mapStateToProps, map allPosts to state which is an array.
class App extends Component {
render() {
return (
<div className="App">
<Input />
<List posts={this.props.allPosts} />
</div>
);
}
}
const mapStateToProps = state => {
return {
allPosts: state
};
};
export default connect(mapStateToProps, null)(App);
Related
I have a Card component that shows a product and then a compare check box. If this is checked then the the item should reflect that in all areas it is shown (with a checked box). This is fine from page to page however there are a few spots that item may be displayed on a carousel and on a list for relative items. So then when that list item is clicked the same item in the carousel on the same 'page' does not update until we refresh. The code is fairly large but we have a button component inside the card component which has a function in the useEffect to watch for changes (which works fine until the item is shown twice on one page) but again it wont update unless we refresh.
I'm sharing the code for the compare button... that's really where I feel the changes are needed:
BUTTON COMPONENT
export interface LikeCompareButtonProps {
products: any[];
}
export default function LikeCompareButton({ products }: LikeCompareButtonProps) {
const dispatch = useDispatch();
const [checked, setChecked] = useState(false);
const checkProducts: any[] = useAppSelector((state) => state?.compareItems?.products)
|| [];
const checkForChange = (products: any) => {
if (compareView) {
setChecked(true);
}
if (!compareView) {
const checked = checkProducts?.find((x) => x.id === products?.id);
if (checked) {
setChecked(true);
} else {
setChecked(false);
}
}
};
const handleChange = (products: {}) => {
if (!checked) {
dispatch(setCompareItem(products));
dispatch(compareItemIncreaseCounter());
setChecked(true);
}
if (checked) {
dispatch(deleteCompareItem(products));
dispatch(compareItemDecreaseCounter());
setChecked(false);
}
getCompareItem();
};
useEffect(() => {
dispatch(getCompareItem());
dispatch(getCompareCounter());
checkForChange(products);
}, [checked]);
// hoping checked here would trigger every time it changes....
// parent component is card one. it maps through an Array -> products
return (
<div>
<button onClick={() => handleChange(products)}>
<div>
{checked && <CheckIcon className="text-white font-bold absolute h-4 w-4" />}
</div>
{!checked ? "Compare" : "Remove"}
</button>
</div>
);
}
CARD COMPONENT import Link from "next/link";
import LikeCompareButton from "../../button/like-compare-button";
export interface SmallProductCardProps {
products: any[];
}
export default function SmallProductCard({products}:
SmallProductCardProps) {
return (
<>
{products?.map((product: any, index: number) => (
<div key={product.id} >
<Link
href={
pathname: `/store/category/d/${product.id}`,
query: { item: product.id }
}}>
<a>
<img
src="http://www.innovativeengsystems.com/wp-c
ontent/uploads/2017/11/no-img-portrait.png"
alt={product.id}
/>
)}
</a>
</Link>
<LikeCompareButton
products={products[index]}
/>
</div>
))}
</>
);
}
HOME: WHERE PRODUCTS IS DEFINED
const Home: NextPage = () => {
const dispatch = useDispatch();
const products: any[] = useAppSelector((state) =>
state?.products?.products?.data);
useEffect(() => {
dispatch(getProductList()); //API RENDERED DATA
}, []);
return (
<>
<SectionLayout hero={true}>
<InnerGridViewLayout colSize={"col-span-4 grid-cols-2 grid-
rows-2 pt-0"}>
<SmallProductCard
products={products}
/>
</InnerGridViewLayout>
</SectionLayout>
</>
);
};
export default Home;
Adding REDUX work
ACTIONS
export const setCompareItem = (products: any) => {
return {
type: SET_COMPARE_ITEM,
payload: products
};
};
export const setCompareItemSuccessful = (products: any) => {
localStorage.setItem("item", JSON.stringify(products.payload));
const existingItems: any[] =
JSON.parse(localStorage.getItem("compareItems")!) ?? [];
existingItems?.push(products.payload);
localStorage.setItem("compareItems",
JSON.stringify(existingItems));
return {
type: SET_COMPARE_ITEM_SUCCESSFUL,
payload: products
};
};
export const getCompareItem = () => {
return {
type: GET_COMPARE_ITEM,
payload: []
};
};
export const getCompareItemSuccessful = () => {
var products: any[] = JSON.parse(localStorage.getItem("compareItems")!);
return {
type: GET_COMPARE_ITEM_SUCCESSFUL,
payload: products
};
};
REDUCER
import {
ERROR_COMPARE,
SET_COMPARE_ITEM,
SET_COMPARE_ITEM_SUCCESSFUL,
GET_COMPARE_ITEM,
GET_COMPARE_ITEM_SUCCESSFUL,
DELETE_COMPARE_ITEM
} from "./actionTypes";
const compareItemsState = {
products: [],
loading: false,
message: ""
};
const reducer = (state = compareItemsState, action) => {
switch (action.type) {
case SET_COMPARE_ITEM:
state = {
...state,
products: [action.payload],
message: "item added"
};
break;
case SET_COMPARE_ITEM_SUCCESSFUL:
state = {
...state,
products: [action.payload],
message: "set item success"
};
break;
case GET_COMPARE_ITEM:
state = {
...state,
products: action.payload,
message: "item found success"
};
break;
case GET_COMPARE_ITEM_SUCCESSFUL:
state = {
...state,
products: action.payload,
message: "found item"
};
break;
case DELETE_COMPARE_ITEM:
state = {
...state,
products: action.payload,
message: "item deleted"
};
break;
case ERROR_COMPARE:
state = {
...state,
compareCounter: state.compareCounter,
message: "Error adding message"
};
break;
default:
state = { ...state };
break;
}
return state;
};
export default reducer;
SAGA.tsx
import { takeEvery, fork, put, all, call } from "redux-
saga/effects";
import {
SET_COMPARE_ITEM,
SET_COMPARE_ITEM_SUCCESSFUL,
DELETE_COMPARE_ITEM,
GET_COMPARE_ITEM_SUCCESSFUL,
GET_COMPARE_ITEM,
} from "./actionTypes";
import {
compareError,
setCompareItem,
setCompareItemSuccessful,
deleteCompareItem,
getCompareItem,
getCompareItemSuccessful,
} from "./actions";
function* setCompareItems(product: any) {
try {
yield put(setCompareItem(product));
} catch (error: any) {
yield put(compareError("Item not added, please try again."));
}
}
function* setCompareItemsSuccessful(products: any) {
try {
yield put(setCompareItemSuccessful(products));
} catch (error: any) {
yield put(compareError("Item not removed, please try
again."));
}
}
function* getCompareItems() {
try {
yield put(getCompareItem());
} catch (error: any) {
yield put(compareError("please try again."));
}
}
function* getCompareItemsSuccessful() {
try {
yield put(getCompareItemSuccessful());
} catch (error: any) {
yield put(compareError("please try again."));
}
}
function* setDeleteItems(products: any) {
try {
yield put(deleteCompareItem(products));
} catch (error: any) {
yield put(compareError("Item removed"));
}
}
export function* watchSetCompareItemSuccess() {
yield takeEvery(SET_COMPARE_ITEM_SUCCESSFUL, setCompareItems);
}
export function* watchSetCompareItem() {
yield takeEvery(SET_COMPARE_ITEM, setCompareItemsSuccessful);
}
export function* watchGetCompareItemSuccess() {
yield takeEvery(GET_COMPARE_ITEM_SUCCESSFUL, getCompareItems);
}
export function* watchGetCompareItem() {
yield takeEvery(GET_COMPARE_ITEM, getCompareItemsSuccessful);
}
export function* watchDeleteCompareItem() {
yield takeEvery(DELETE_COMPARE_ITEM, setDeleteItems);
}
function* compareItemStoreSaga() {
yield all([
fork(watchSetCompareItem),
fork(watchGetCompareItem),
]);
}
export default compareItemStoreSaga;
EDIT
The component that state is not rendering in is called TournamentShow, which calls the state and whatever functions I need to use for the Show page.
Nested within it is a conditional to call one of 3 pages, based on
Tournament.Status === "Open",
Tournament.Status === "Closed", and
Tournament.Status === "Complete"
Tournament Show:
import React, { Component } from 'react';
import { SignUpPage, HostUI, StartBracket, Results } from './TournamentScreens';
import {
showTournament,
addParticipant,
closeTournament,
shuffleParticipants
} from '../../actions/tournamentActions';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Spinner } from 'reactstrap';
class TournamentShow extends Component {
constructor(props) {
super(props);
this.onSignUp = this.onSignUp.bind(this);
this.onStartTournament = this.onStartTournament.bind(this);
this.onShuffleParticipants = this.onShuffleParticipants.bind(this);
};
componentDidMount() {
const id = this.props.match.params.id;
this.props.showTournament(id);
};
static propTypes = {
tournament: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired
};
onSignUp(tournamentId, user) {
this.props.addParticipant(tournamentId, user);
};
onShuffleParticipants(array) {
let currentIndex = array.length, temporaryValue, randomIndex;
while(0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
};
onStartTournament(tourneyId) {
const { participants } = this.props.tournament.showTournament;
// Randomize participants
let reorderedParticipants = [];
const shuffledParticipants = this.onShuffleParticipants(participants);
shuffledParticipants.forEach(participant => {
reorderedParticipants.push(participant);
});
// Send new participants list to backend
this.props.shuffleParticipants(tourneyId, reorderedParticipants);
// Set Status to "Closed"
this.props.closeTournament(tourneyId);
};
render() {
console.log(this.props.tournament)
const loading = this.props.tournament.loading || !this.props.tournament.showTournament;
if(loading) {
return <Spinner color="light" />
} else {
if(this.props.tournament.showTournament.status === "Complete") {
return (
<Results />
);
} else if(this.props.tournament.showTournament.status === "Closed") {
return (
<div>
<HostUI
tournament={this.props.tournament.showTournament}
/>
<StartBracket
tournament={this.props.tournament.showTournament}
/>
</div>
);
} else {
return (
<SignUpPage
tournament={this.props.tournament.showTournament}
auth={this.props.auth}
onSignUp={this.onSignUp}
onStartTournament={this.onStartTournament}
/>
);
}
};
};
};
const mapStateToProps = state => ({
tournament: state.tournament,
auth: state.auth
});
export default connect(mapStateToProps,
{ showTournament, addParticipant, closeTournament, shuffleParticipants }
)(TournamentShow);
Tournament Show Screens:
import React from 'react';
import moment from 'moment';
import { TournamentSignUp, StartTournament } from './resources/buttons';
import { TournamentRules } from './resources/rulesets';
import { Button } from 'reactstrap';
import { Link } from 'react-router-dom';
// Status === "Open"
export const SignUpPage = ({ tournament, auth, onSignUp, onStartTournament }) => {
};
// Status === "Closed"
export const HostUI = ({ tournament }) => {
const { players } = tournament.bracket;
return (
<div style={{color:"lightgrey"}}>
<h1>Host UI</h1>
{
players && players.map(player => (
<div>
{player.username}
</div>
))
}
</div>
);
};
export const StartBracket = ({ tournament }) => {
const { title, hostedBy, participants } = tournament;
return (
<div className="text-center" style={{color:"lightgrey", backgroundColor: "#333333"}}>
<h1>{ title }</h1>
<h4>By { hostedBy }</h4>
<h4>{participants && participants.length}-player bracket</h4>
<br /><Link to="/">Back to Tournaments main page</Link>
</div>
);
};
// Status === "Complete"
export const Results = () => {
};
Status===Closed shows both of those central components.
HostUI renders just the players array (which had just been updated right before the status switch/re-render)
StartBracket shows stuff from ShowTournament, which is all data that had already been set in the state
ORIGINAL --------------------------
I'll mark with // comment which case does not work
import {
GET_TOURNAMENTS,
SHOW_TOURNAMENT,
ADD_TOURNAMENT,
ADD_TOURNAMENT_FAIL,
EDIT_TOURNAMENT,
EDIT_TOURNAMENT_FAIL,
DELETE_TOURNAMENT,
TOURNAMENTS_LOADING,
TOURNAMENT_LOADING,
USER_JOINS_TOURNAMENT,
TOURNAMENT_SIGN_UP_FAIL,
TOURNAMENT_STATUS_UPDATE,
TOURNAMENT_STATUS_FAILED,
SHUFFLE_PARTICIPANTS,
SHUFFLE_FAILED
} from '../actions/types';
const initialState = {
tournaments: [],
showTournament: {},
loading: false,
};
export default function(state = initialState, action) {
switch(action.type) {
case GET_TOURNAMENTS:
return {
...state,
tournaments: action.payload,
loading: false
};
case SHOW_TOURNAMENT:
return {
...state,
showTournament: action.payload,
loading: false
};
case ADD_TOURNAMENT:
return {
...state,
tournaments: [action.payload, ...state.tournaments]
};
case DELETE_TOURNAMENT:
return {
...state,
tournaments: state.tournaments.filter(tournament => tournament._id !== action.payload)
};
case TOURNAMENTS_LOADING:
case TOURNAMENT_LOADING:
return {
...state,
loading: true
};
case USER_JOINS_TOURNAMENT:
return {
...state,
...state.showTournament.participants.push(action.payload)
};
case TOURNAMENT_STATUS_UPDATE: // Occurs with SHUFFLE_PARTICIPANTS, which doesn't work
return {
...state,
...state.showTournament.status = action.payload
};
case SHUFFLE_PARTICIPANTS: // Does not work
return {
...state,
...state.showTournament.bracket.players.push(action.payload)
}
case EDIT_TOURNAMENT:
case ADD_TOURNAMENT_FAIL:
case EDIT_TOURNAMENT_FAIL:
case TOURNAMENT_SIGN_UP_FAIL:
case TOURNAMENT_STATUS_FAILED:
case SHUFFLE_FAILED:
return {
...state,
}
default:
return state;
};
};
Most of that works.
The ones I'm sure I screwed up are TOURNAMENT_STATUS_UPDATE and SHUFFLE_PARTICIPANTS, although status update works as intended.
This is a tournament app whose show page renders 3 different components based on showTournament.status
...
if(loading) {
return <Spinner color="light" />
} else {
if(this.props.tournament.showTournament.status === "Complete") {
return (
<Results />
);
} else if(this.props.tournament.showTournament.status === "Closed") {
return (
<div>
<HostUI
tournament={this.props.tournament.showTournament}
/>
<StartBracket
tournament={this.props.tournament.showTournament}
/>
</div>
);
} else {
return (
<SignUpPage
tournament={this.props.tournament.showTournament}
auth={this.props.auth}
onSignUp={this.onSignUp}
onStartTournament={this.onStartTournament}
/>
);
}
};
Component button:
randomizes Tournament.participants and sends it to Tournament.bracket.players
sets Tournament.status === "Closed"
That updates the page and renders the Status: "Closed" page correctly.
The problem is, it only displays whatever I already had loaded in the state. (stuff from SHOW_TOURNAMENT)
The bracket.players array I sent the randomized user list to doesn't display until I refresh the page.
You need to shallow copy each level of state you are updating. Also, ...state.showTournament.bracket.players.push(action.payload) will simply attempt to spread in the return value of the push which is just the new length of the array. This isn't what you want.
case TOURNAMENT_STATUS_UPDATE:
return {
...state,
showTournament: {
...state.showTournament,
status: action.payload,
},
};
case SHUFFLE_PARTICIPANTS:
return {
...state,
showTournament: {
...state.showTournament,
bracket: {
...state.showTournatment.bracket,
players: [...state.showTournament.bracket.players, ...action.payload], // spread payload array
},
},
}
Drew's solution worked, it was just that, in order to pass an array into another array, the syntax is
players: [...state.showTournament.bracket.players. ...action.payload]
rather than players: [...state.showTournament.bracket.players, action.payload]
Good day
I have An action That I am trying to use in my component, My FetchPosts() function will work and map to state, but when I use my other Action FetchUserPosts() I can see in the redux dev tools that the action dispatches, but in State nothing is there and the state doesn't change at all.
It could be something with my action but im not sure.
postAction.js
import { FETCH_POSTS, NEW_POSTS, FETCH_POST_FOR_APPROVAL,
FETCH_POSTS_FROM_USER } from './types';
export const fetchPostsFromUser = (id) => dispatch => {
Promise.all([fetch('http://10.6.254.22:5000/posts/1' + id)])
.then(([res1]) => {
return Promise.all([res1.json()])
})
.then(([posts]) => dispatch ({
type: FETCH_POSTS_FROM_USER,
payload: posts
}));
}
Types.js
export const FETCH_POSTS_FROM_USER = 'FETCH_POSTS_FROM_USER';
PostReducer.js
import { FETCH_POSTS, NEW_POSTS, FETCH_POST_FOR_APPROVAL,
FETCH_POSTS_FROM_USER } from '../actions/types';
const initialState = {
items: [],
item: {}
}
export default function (state = initialState, action ){
switch(action.type) {
case FETCH_POSTS:
console.log('currently reducing')
return{
...state,
items: action.payload
}
case NEW_POSTS:
return{
...state,
item: action.payload
}
case FETCH_POST_FOR_APPROVAL:
return{
...state,
items: action.payload
}
case FETCH_POSTS_FROM_USER:
return{
...state,
items: action.payload
}
default:
return state;
}
}
My Component I want the function to work with so i can map the props.
class ProfilePostBody extends Component {
constructor(props){
super(props)
this.state = {
commentBody: null,
postId: null,
giphyUrl: null,
postPicture: null,
userId: null,
userIdto: null,
userIdName: null,
userIdtoName:null,
// postBody: null,
// giphyUrl: null,
// userIdto: null,
// userIdName: null,
// userIdtoName:'Julio',
displayGifPicker: false
}
}
componentDidMount(){
this.props.fetchPostsFromUser();
this.props.fetchPosts();
}
render () {
return (
// <Grid item xl={6}>
<div>
{this.props.posts.map((post, index) =>
<PostBodyTemplate key={index} postId={post.id} onSubmit=
{this.handleSubmit} onChange={e =>
this.handleInputChange(e,post.id,post.userId,post.userIdName)} title=
{post.title} postBody={post.postBody}
giphyUrl = {post.giphyUrl} userWhoPosted={post.userIdName}
commentBody={post.commentBody} userIdtoName={post.userIdtoName}
userIdName={post.userIdName} />
)}
</div>
)
}
}
ProfilePostBody.propTypes = {
fetchPostsFromUser: PropTypes.func.isRequired,
fetchPosts: PropTypes.func.isRequired,
posts: PropTypes.array.isRequired
}
const mapStateToProps = state =>({
posts: state.posts.items
})
export default connect(mapStateToProps, { fetchPosts, fetchPostsFromUser
})
(ProfilePostBody);
fetchPosts will work, but if i comment it out and try just the fetchPostsFromUser() it will not work.
Help me please solve this issue.
I use redux and react-redux to control state in my app.
But when I try to change styles in my Component depending in the value from redux store, it react with delay. When I add new Item and click the list and expect its color being changed, it does this only after I add another item, so that it always delays.
Here is my reducer
export const items = (state = [], action) => {
switch(action.type) {
case 'ADD_ITEM':
const newItem = {
title: action.title,
id: action.id,
selected: action.selected,
comments: action.comments
};
return [
...state,
newItem
];
case 'REMOVE_ITEM':
return state.filter(({ id }) => id !== action.id);
case 'SELECT_ITEM':
state.map((item) => {
if (item.id === action.id) {
return [
...state,
item.selected = true
];
} else {
return [
...state,
item.selected = false
];
}
});
default:
return state;
}
};
Here is my component which I want to react on every change of the redux store
import React from 'react';
import { connect } from 'react-redux';
import { removeItem, selectItem } from '../actions/items';
import { Badge, ListGroup, ListGroupItem, Button } from 'reactstrap';
const stylesForActiveItem = {
borderLeft: '4px solid red',
transition: 'all .5s',
marginLeft: '-4px',
borderRadius: '0'
}
class Item extends React.Component {
constructor(props) {
super(props);
}
render() {
const { removeItem, selectItem, id, title, selected } = this.props;
return (
<ListGroup className="Item">
<ListGroupItem
onClick={() => selectItem({ id })}
className={selected ? 'Item__text active-item' :
'Item__text'}
>{title} <Badge className="Item__badge">14</Badge>
</ListGroupItem>
<Button className="Item__btn" onClick={() => removeItem({ id
})}>Delete</Button>
</ListGroup>
);
}
}
const mapDispatchToProps = (dispatch) => ({
removeItem: (id) => dispatch(removeItem(id)),
selectItem: (id) => dispatch(selectItem(id))
})
export default connect(null, mapDispatchToProps)(Item);
state.map((item) => {
if (item.id === action.id) {
return [
...state,
item.selected = true
];
} else {
return [
...state,
item.selected = false
];
}
});
//I guess you need to do something like this
state.map((item) => {
if (item.id === action.id) {
return {...item, selected:true}
} else {
return {...item, selected:false}
}
});
Since even though map returns new array, internal object should also not get mutated. That is why we spread and create a new item object inside.
There is no need to create arrays again in map with entire state. That will just change your state structure instead of just changing a boolean.
I've been battling with this for too long, can someone point me to the right direction?
The issue: When i create a list I am able to update/delete it. I am also able to add items to it and update/delete these items. When I add another list, the items from the former gets carried over to the latter then I am unable to edit the items. If I delete the List and don't refresh the browser the items are still in the list. I need a way to tie the two together in a way that a list only knows about its items
Thanks in advance for the help.
/actions/lists.js
export const CREATE_LIST = 'CREATE_LIST'
export function createList(list) {
return {
type: CREATE_LIST,
id: uuid.v4(),
items: list.items || [],
...list
}
}
export const CONNECT_TO_LIST = 'CONNECT_TO_LIST'
export function connectToList(listId, itemId) {
return {
type: CONNECT_TO_LIST,
listId,
itemId
}
}
export const DISCONNECT_FROM_LIST = 'DISCONNECT_FROM_LIST'
export function disconnectFromList(listId, itemId) {
return {
type: DISCONNECT_FROM_LIST,
listId,
itemId
}
}
/actions/items.js
export const CREATE_ITEM = 'CREATE_ITEM'
export function createItem(item) {
return {
type: CREATE_ITEM,
item: {
id: uuid.v4(),
...item
}
}
}
export const UPDATE_ITEM = 'UPDATE_ITEM'
export function updateItem(updatedItem) {
return {
type: UPDATE_ITEM,
...updatedItem
}
}
/reducers/lists.js
import * as types from '../actions/lists'
const initialState = []
export default function lists(state = initialState, action) {
switch (action.type) {
case types.CREATE_LIST:
return [
...state,
{
id: action.id,
title: action.title,
items: action.items || []
}
]
case types.UPDATE_LIST:
return state.map((list) => {
if(list.id === action.id) {
return Object.assign({}, list, action)
}
return list
})
case types.CONNECT_TO_LIST:
const listId = action.listId
const itemId = action.itemId
return state.map((list) => {
const index = list.items.indexOf(itemId)
if(index >= 0) {
return Object.assign({}, list, {
items: list.items.length > 1 ? list.items.slice(0, index).concat(
list.items.slice(index + 1)): []
})
}
if(list.id === listId) {
return Object.assign({}, list, {
items: [...list.items, itemId]
})
}
return list
})
case types.DISCONNECT_FROM_LIST:
return state.map((list) => {
if(list.id === action.listId) {
return Object.assign({}, list, {
items: list.items.filter((id) => id !== action.itemId)
})
}
return list
})
default:
return state
}
}
/reducers/items.js
import * as types from '../actions/items'
const initialState = []
export default function items(state = initialState, action) {
switch (action.type) {
case types.CREATE_ITEM:
return [ ...state, action.item ]
case types.UPDATE_ITEM:
return state.map((item) => {
if(item.id === action.id) {
return Object.assign({}, item, action)
}
return item
})
case types.DELETE_ITEM:
return state.filter((item) => item.id !== action.id )
default:
return state
}
}
/components/List.jsx
import React from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import Items from './Items'
import Editor from './Editor'
import * as listActionCreators from '../actions/lists'
import * as itemActionCreators from '../actions/items'
export default class List extends React.Component {
render() {
const { list, updateList, ...props } = this.props
const listId = list.id
return (
<div {...props}>
<div className="list-header"
onClick={() => props.listActions.updateList({id: listId, isEditing: true})}
>
<div className="list-add-item">
<button onClick={this.addItem.bind(this, listId)}>+</button>
</div>
<Editor
className="list-title"
isEditing={list.isEditing}
value={list.title}
onEdit={title => props.listActions.updateList({id: listId, title, isEditing: false})}
/>
<div className="list-delete">
<button onClick={this.deleteList.bind(this, listId)}>x</button>
</div>
</div>
<Items
items={this.listItems}
onValueClick={id => props.itemActions.updateItem({id, isEditing: true})}
onEdit={(id, text) => props.itemActions.updateItem({id, text, isEditing: false})}
onDelete={itemId => this.deleteItem(listId, itemId)}
/>
</div>
)
}
listItems() {
props.list.items.map(id => state.items[
state.items.findIndex(item => item.id === id)
]).filter(item => item)
}
deleteList(listId, e) {
e.stopPropagation()
this.props.listActions.deleteList(listId)
}
addItem(listId, event) {
event.stopPropagation()
const item = this.props.itemActions.createItem({
text: 'New Shopping Item'
})
this.props.listActions.connectToList(listId, item.id)
}
deleteItem(listId, itemId) {
this.props.listActions.disconnectFromList(listId, itemId)
this.props.itemActions.deleteItem(itemId)
}
}
function mapStateToProps(state) {
return {
lists: state.lists,
items: state.items
}
}
function mapDispatchToProps(dispatch) {
return {
listActions: bindActionCreators(listActionCreators, dispatch),
itemActions: bindActionCreators(itemActionCreators, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(List)
/components/List.jsx
import React from 'react'
import List from './List.jsx'
export default ({lists}) => {
return (
<div className="lists">{lists.map((list) =>
<List className="list" key={list.id} list={list} id={list.id} />
)}</div>
)
}
/components/Items.jsx
import React from 'react'
import { connect } from 'react-redux'
import Editor from './Editor'
import Item from './Item'
export default class Items extends React.Component {
render () {
const {items, onEdit, onDelete, onValueClick, isEditing} = this.props
return (
<ul className="items">{items.map(item =>
<Item
className="item"
key={item.id}
id={item.id}
isEditing={item.isEditing}>
<Editor
isEditing={item.isEditing}
value={item.text}
onValueClick={onValueClick.bind(null, item.id)}
onEdit={onEdit.bind(null, item.id)}
onDelete={onDelete.bind(null, item.id)}
/>
</Item>
)}</ul>
)
}
}
export default connect(
state => ({
items: state.items
})
)(Items)
/components/Item.jsx
import React from 'react'
export default class Item extends React.Component {
render() {
const { id, isEditing, ...props } = this.props
return (
<li {...props}>{props.children}</li>
)
}
}
/components/App.jsx
class App extends React.Component {
handleClick = () => {
this.props.dispatch(createList({title: "New Shopping List"}))
}
render() {
const lists = this.props.lists
return (
<div>
<button
className="add-list"
onClick={this.handleClick}>Add Shopping List</button>
<Lists lists={lists}/>
</div>
)
}
}
export default connect(state => ({ lists: state.lists }))(App)
Assuming this is all meant to operate on a single array at a time, this part looks pretty suspicious:
case types.CREATE_LIST:
return [
...state,
{
id: action.id,
title: action.title,
items: action.items || []
}
]
That ...state is expanding whatever existing array there is into the new array that you're returning, and it doesn't sound like that's the behavior you actually want. My first guess is that when you create a new list, you probably want to just return the one new item inside, not the entire old list contents plus that new item.
Some of your other immutable-style update code also looks sort of complex. I know that "update a thing in the middle of an array" isn't always easy to deal with. You might want to take a look at this SO post on updating immutable data, which lists several ways to approach it. I also have a links repo that catalogs Redux-related libraries, and it has a list of immutable data management libraries that might make things easier for you.
Though #markerikson 's tip was part of the issue i was having, it didn't solve it completely. I had to fix my mapStateToProps to this
function mapStateToProps(state, props) {
return {
lists: state.lists,
listItems: props.list.items.map(id => state.items[
state.items.findIndex(item => item.id === id)
]).filter(item => item)
}
}
and remove the connect from previous implementation for the items alone in my Items.jsx