Updating array value inside array of objects - reactjs

I have an array "movies" of objects like this:
{
id: "some id",
title: "some title",
actors: []
}
As you can see, the actors array stays empty, because I don't add actors while I am adding a movie. I want to add actors later on, while being on http://localhost:3000/movies/<movieId> .
I do this by this Form. Im just selecting a actor, from already existing lists of actors.
const PostForm = ({ addActorToMovie, movie, actors, history }, props) => {
const handleSubmit = (values) => {
addActorToMovie(values);
// history.push(`/movies/${movie.id}`);
};
let actorsList =
actors.length > 0 &&
actors.map((item, i) => {
return (
<option value={`${item.firstName}, ${item.lastName}`}>
{item.firstName + item.lastName}{" "}
</option>
);
});
return (
<div>
<h3>Add Actor</h3>
<Formik
initialValues={{
actor: "",
}}
onSubmit={(values) => handleSubmit(values)}
enableReinitialize={true}
>
<Form>
<Field name="actor" component="select">
<option value="null"> </option>
{actorsList}
</Field>
<button type="submit">Zatwierdz</button>
</Form>
</Formik>
</div>
);
};
const mapStateToProps = (state, props) => {
return {
movie: state.movie,
actors: state.actors,
};
};
const mapDispatchToProps = {
addActorToMovie,
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(PostForm)
);
The form works. Although instead of adding value to my actors array. It creates a new object under movies array which looks like this:
actors: {actor: 'Some actor name'}
My reducers looks like this. Probably this is the source of the problem, but I don't really know how to do this.
export const movieReducer = (state = [], action) => {
switch (action.type) {
case "MOVIE_ADD":
return [...state, action.payload];
case "MOVIE_DELETE":
return [...state.filter((el) => el.id !== action.payload.id)];
case "MOVIEACTOR_ADD":
return {
...state,
actors: action.payload
};
default:
return state;
}
};
What I want to do is of course push the actor which i enter in my Formik to an array of specified movie.

Instead of declaring a new actors property that is only the payload, you want to append it to the existing actors array. However, you will first need to know which movie object you are updating, so this also needs to be sent in the action payload.
Update the submit handler to send both the movie id and the actor to be added.
const handleSubmit = ({ actor }) => {
addActorToMovie({
movieId: movie.id,
actor,
});
};
Update the reducer case to map the movies array, which appears to be state in movieReducer. Shallow copy the movies state array, and for the matching movie id, shallow copy and append the actor to the actors array.
case "MOVIE_ACTOR_ADD":
const { actor, movieId } = action.payload;
return state.map(movie => movie.id === movieId
? {
...movie, // <-- shallow copy movie object
actors: [
...movie.actors, // <-- shallow copy existing array
actor // <-- append new actor payload
],
}
: movie
);
When adding a movie to state, ensure the actors array property exists. If you aren't passing it the MOVIE_ADD action payload then handle it in the reducer when adding a movie.
case "MOVIE_ADD":
return [
...state,
{ ...action.payload, actors: [] },
];

Related

Element is not removed from array

I am trying to delete an actor from actors array which is inside movie object.
Firstly, I map actors to display actors one by one and adding a remove action to button.
Whole ActorsList component
const PostList = ({ movie, removeActorFromMovie }, props) => {
return (
<div>
<h3>Actors in film </h3>
{movie.actors.map((actor, i) => (
<div key={actor}>
{actor}
<button onClick={() => removeActorFromMovie(actor)}>Usuń</button>
</div>
))}
</div>
);
};
const mapStateToProps = (state, props) => {
return {
movie: state.movies.find((movie) => movie.id === props.match.params.id),
};
};
const mapDispatchToProps = {
removeActorFromMovie,
};
export default withRouter(
connect
(mapStateToProps, mapDispatchToProps)(PostList)
);
Action:
export const removeActorFromMovie = (payload) => ({
type: "MOVIE_ACTOR_DELETE",
payload,
});
Reducer:
case "MOVIE_ACTOR_DELETE":
return [...state.filter((el) => el !== action.payload)];
Although, the payload wokrs great and returns the array item. It does not remove it from an array.
My movies array with movie under index 0:
movies: Array(1)
0:
actors: (2) ['Robert XYZ', 'Jake XYZ']
director: "Quentino TARANTINO, TARANTINO"
id: "352cc51e-87c4-46ec-84a0-9f308ec1a71a"
productionYear: "123"
title: "Titanic"
and payload:
payload: "Robert XYZ"
type: "MOVIE_ACTOR_DELETE"
and as I said, this doest not remove the actor from actors array which is under index 0 in movies array
Update:
I'm supposing your state is something like this:
state: {
movies: [...]
}
If you want to remove an actor from a movie you should also pass the payload a movie to remove the actor from.
removeActorFromMovie(actor, movie)
then in the reducer:
const { movie, actor } = action.payload;
const nextMovie = {
...movie,
actors: movie.actors.filter((a) => a !== actor)
};
const movieIndex = state.movies.findIndex((m) => m.id === movie.id);
return {
...state,
movies: [
...state.movies.slice(0, movieIndex),
nextMovie,
...state.movies.slice(movieIndex + 1)
]
};
OLD ANSWER (If you instead want to remove a movie):
If you want to remove a movie based on its actors you filter function will not work. You are trying to remove an item whose value is "Robert XYZ" but the element passed to you filter function is a movie.
if the state is like this:
movies: [
{
actors: ['Robert XYZ', 'Jake XYZ']
director: "Quentino TARANTINO, TARANTINO"
id: "352cc51e-87c4-46ec-84a0-9f308ec1a71a"
productionYear: "123"
title: "Titanic"
}
]
then you can do:
const nextItems = state.filter(movie => !movie.actors.includes(action.payload))
return nextItems;
There is no need to create a new copy of the filtered array with the spread operator because filter return a new copy of the original array.

Delete multiple item from array - Redux State

I'm working on react app with redux. I want to delete multiple item from array. I write below code in my reducer which delete single item from array but i want to delete multiple item.
case DELETE_LINK:
let dltLink = state.filter(item => {
return item._id !== action.data._id
})
return {
...state,
parentFolderlinks: dltLink
};
It seems you want to filter links from state.parentFolderlinks, say you have the ids in action.data.ids, you could
case DELETE_LINK:
const parentFolderlinks = state.parentFolderlinks.filter(item => {
return !action.data.ids.includes(item._id);
});
return {
...state,
parentFolderlinks
};
On what basis would you like to filter items? I assume that multiple items will not have the same id.
Below example shows how we can filter multiple items in redux. In this case, foods state with items that has type as fruit and removes everything else.
// initial state with all types of foods
const initialState = {
"foods": [
{
name: "apple",
type: "fruit"
},
{
name: "orange",
type: "fruit"
},
{
name: "broccoli",
type: "vegetable"
},
{
name: "spinach",
type: "vegetable"
},
]
}
// sample reducer that shows how to delete multiple items
export default (state = initialState, { type, payload }) => {
switch (type) {
// delete multiple items that does not have type fruit
// i.e both brocolli and spinach are removed because they have type vegetable
case DELETE_ITEMS_WITHOUT_TYPE_FRUIT:
const onlyFruits = state.foods.filter(food => food.type === "fruit");
return {
...state,
foods: onlyFruits
}
}
}
you could map over the state and run it through a function that works out if you want to keep it or not (I don't know what your logic is for that) then return the array at the end
const keepThisItem =(item) => {
return item.keep
}
case DELETE_LINK:
let itemsToKeep = []
let dltLink = state.map(item => {
if(keepThisItem(item){
itemsToKeep.push(item)
}
return itemsToKeep
})

How to Add filter into a todolist application in Reactjs with using .filter

im new to react, trying to make an todolist website, i have the add and delete and displaying functionality done, just trying to add an search function, but i cant seem to get it working, where as it doesn't filter properly.
i basically want to be able to filter the values on the todos.title with the search value. such as if i enter an value of "ta" it should show the todo item of "take out the trash" or any item that matches with that string.
when i try to search, it gives random outputs of items from the filtered, i am wondering if my filtering is wrong or if i am not like displaying it correctly.
ive tried to pass the value into todo.js and display it there but didn't seem that was a viable way as it it should stay within App.js.
class App extends Component {
state = {
todos: [
{
id: uuid.v4(),
title: "take out the trash",
completed: false
},
{
id: uuid.v4(),
title: "Dinner with wife",
completed: true
},
{
id: uuid.v4(),
title: "Meeting with Boss",
completed: false
}
],
filtered: []
};
// checking complete on the state
markComplete = id => {
this.setState({
todos: this.state.filtered.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed;
}
return todo;
})
});
};
//delete the item
delTodo = id => {
this.setState({
filtered: [...this.state.filtered.filter(filtered => filtered.id !== id)]
});
};
//Add item to the list
addTodo = title => {
const newTodo = {
id: uuid.v4(),
title,
comepleted: false
};
this.setState({ filtered: [...this.state.filtered, newTodo] });
};
// my attempt to do search filter on the value recieved from the search field (search):
search = (search) => {
let currentTodos = [];
let newList = [];
if (search !== "") {
currentTodos = this.state.todos;
newList = currentTodos.filter( todo => {
const lc = todo.title.toLowerCase();
const filter = search.toLowerCase();
return lc.includes(filter);
});
} else {
newList = this.state.todos;
}
this.setState({
filtered: newList
});
console.log(search);
};
componentDidMount() {
this.setState({
filtered: this.state.todos
});
}
componentWillReceiveProps(nextProps) {
this.setState({
filtered: nextProps.todos
});
}
render() {
return (
<div className="App">
<div className="container">
<Header search={this.search} />
<AddTodo addTodo={this.addTodo} />
<Todos
todos={this.state.filtered}
markComplete={this.markComplete}
delTodo={this.delTodo}
/>
</div>
</div>
);
}
}
export default App;
search value comes from the header where the value is passed through as a props. i've checked that and it works fine.
Todos.js
class Todos extends Component {
state = {
searchResults: null
}
render() {
return (
this.props.todos.map((todo) => {
return <TodoItem key={todo.id} todo = {todo}
markComplete={this.props.markComplete}
delTodo={this.props.delTodo}
/>
})
);
}
}
TodoItem.js is just the component that displays the item.
I not sure if this is enough to understand the issue 100%, i can add more if needed.
Thank you
Not sure what is wrong with your script. Looks to me it works fine when I am trying to reconstruct by using most of your logic. Please check working demo here: https://codesandbox.io/s/q9jy17p47j
Just my guess, it could be there is something wrong with your <TodoItem/> component which makes it not rendered correctly. Maybe you could try to use a primitive element such as <li> instead custom element like <TodoItem/>. The problem could be your logic of markComplete() things ( if it is doing hiding element works ).
Please let me know if I am missing something. Thanks.

React Redux - list of items per some criteria

I faced a problem with React Redux when I need for one page render multiple lists of items from the same API. For example, I have endpoint /items with GET param /items?city=Berlin. I need to render 5 cities lists of items on the frontpage.
Currently I have 1 action to handle API call.
Here is my reducer.js:
...
const initialState = fromJS({
...
items: [],
...
})
export default function reducer (state = initialState, action) {
...
switch (action.type) {
case fetchItemsByCityRoutine.SUCCESS:
return state.update('items', (items) => items.push(...action.payload.data))
...
}
}
saga.js:
function * fetchItemsByCity ({ payload: city }) {
try {
yield put(fetchItemsByCityRoutine.request())
const response = yield call(apiClient.fetchItemsByCity, city)
response.city = city
yield put(fetchItemsByCityRoutine.success(response))
} catch (error) {
yield put(fetchItemsByCityRoutine.failure(extractErrorMessage(error)))
} finally {
yield put(fetchItemsByCityRoutine.fulfill())
}
}
function * watchItemsByCitySaga () {
yield takeEvery(fetchItemsByCityRoutine.TRIGGER, fetchItemsByCity)
}
On the Homepage I render list of cities like this:
const renderCityListSections = () => homepageCityLists.map((city) => <ItemList key={city.cityName} {...city} />)
ItemList component:
class ItemList extends Component {
componentDidMount () {
const { cityName } = this.props
this.props.fetchItemsByCityRoutine(cityName)
}
render () {
const { items, title } = this.props
return (
items.length > 0 &&
<Wrapper>
<SlickSlider>
{items.map((item) => <Item key={item.id} {...item} />)}
</SlickSlider>
</Wrapper>
)
}
}
THE PROBLEM is that current solution makes rerender view too many times because every time I fetch new list for some city it adds to items, so items changes and it leads to trigger rerendering view.
I had a thought to create 5 different Actions for every city, but it's not seems to be reasonable solution.
WHAT is the best approach to render multiple lists of cities on one page?
UPDATE:
So I changed reducer to look like this:
const initialState = fromJS({
...
items: {
Miami: [],
Prague: [],
Melbourne: [],
Venice: [],
Barcelona: [],
},
...
})
export default function reducer (state = initialState, action) {
switch (action.type) {
...
case fetchItemsByCityRoutine.SUCCESS:
return state.updateIn(['items', action.payload.city], (list) => (
list.concat(action.payload.data)
))
...
}
}
so every array is immutable, but again it runs into too much rerendering.
Everybody who has such a problem I ended up with this SOLUTION:
In my reducer I decoupled every item for city like this:
const initialState = fromJS({
...
itemsForBerlin: [],
itemsForAnotherCity: [],
...
})
export default function reducer (state = initialState, action) {
switch (action.type) {
...
case fetchItemsByCityRoutine.SUCCESS:
return state.set(`itemsFor${action.payload.city}`, action.payload.data)
...
}
}

Sorting data in a reducer in redux

Background
I am working on my first project using Redux. I am trying to sort data in a reducer. The reducer has 2 sort methods. One for sorting data based on the timeStamp and one by the VoteScore.
Problem
The reducer fires off, I can console.log() the data and see it is being passed to the correct case when sort is fired off, but the data does not sort on the screen.
**
I can see when I console.log the data that the data is sorting. So it
is just not rerendering.
**
Example of Data
(2) [{…}, {…}]
{timestamp: 1468479767190, voteScore: -5, …}
{timestamp: 1467166872634, voteScore: 6, …}
Example Reducer
function posts(state = [], action) {
switch(action.type) {
case 'SORT_POSTS': {
switch (action.attribute) {
case 'BY_TIMESTAMP':
return state.sort(function(a, b) {
return a.timestamp - b.timestamp;
});
case 'BY_VOTESCORE':
return state.sort(function(a, b) {
return a.voteScore - b.voteScore;
});
default:
return state;
}
}
case SET_POSTS:
return action.posts;
default:
return state;
}
}
Example Dispatch & onClick Method
<button onClick={() => this.props.boundSortPosts(this.state.timeStamp)}>Click MeK</button>
const mapDispatchToProps = (dispatch) => ({
boundSortPosts: (attribute) => dispatch(sortPosts(attribute))
});
Example Action
export function setPosts(posts) {
return {
type: SET_POSTS,
posts,
}
}
export function sortPosts(attribute) {
return {
type: SORT_POSTS,
attribute,
}
}
Initially Rendering Data Like This
render() {
return (
<div>
<Posts />
<div><button onClick={() => this.props.boundSortPosts(this.state.timeStamp)}>Sort by Time Stamp</button></div>
<div><button onClick={() => this.props.boundSortPosts(this.state.voteScore)}>Sort by Vote Score</button></div>
{
this.props.posts.map((post, index) => {
return (
<div key={index}>
<h4>{post.timestamp}</h4>
<h4>{post.voteScore}</h4>
</div>
)
})
}
</div>
)
}
Question
What must I do to cause a rerender at this point?
First and very important thing is you should not mutate your state. The sort() method sorts the elements of an array in place and returns the array.You can create a copy of the state array by calling state.slice() before sorting in your reducer, something like this :
return state.slice().sort(function(a, b) {
return a.timestamp - b.timestamp;
});
Regarding your component not re-rendered, please check your mapStateToProps. It should be something like this:
function mapStateToProps(state} {
return {
posts: state.posts // posts is reducer which is passed to your store.
}
}

Resources