Removing items in array in React using context and reducer - javascript

I've been building an application that pulls data from a database onto a React frontend in the form of items in an array. Each item then has information on it with a button to visit the URL on that item plus a button to delete the item from the database. This functionality works, I have made API's that interact with the database and everything is great. When the delete button is pressed, the event gets removed from the database but it doesn't remove the item from the UI. Could someone please show me where I could be going wrong?
I've created a component for the events, with logic to map all items to a grid on the front end:
const Events = () => {
const eventContext = useContext(EventContext);
const { events, loading } = eventContext;
if (loading) {
return <Spinner />;
} else {
return (
<div style={userStyle}>
{events.map(event => (
<EventItem key={event._id} event={event} />
))}
</div>
);
}
};
const userStyle = {
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gridGap: "1rem"
};
export default Events;
I've then created an EventItem component to show data from each item in the array:
const EventItem = ({ event }) => {
const eventContext = useContext(EventContext);
const { deleteEvent } = eventContext;
const { _id, Client, Keyword, Source, Url, URL, Shodan_URL, Title, Date, Ip, PhishingAgainst, Preview, Verdict, Port, Product, Version, Expires } = event;
const onDelete = e => {
e.preventDefault();
deleteEvent(_id);
}
return (
<div className="card bg-light text-center">
{Client ? <h3>Client: {Client}</h3> : null}
{Keyword ? <p>Keyword: {Keyword}</p> : null}
{Source ? <h4>Source: {Source}</h4> : null}
<hr></hr>
{Url ? <p>URL: {Url}</p> : null}
{URL ? <p>URL: {URL}</p> : null}
{Shodan_URL ? <p>URL: {Shodan_URL}</p> : null}
{Title ? <p>Title: {Title}</p> : null}
{Date ? <p>Date: {Date}</p> : null}
{Ip ? <p>IP: {Ip}</p> : null}
{PhishingAgainst ? <p>Phishing Target: {PhishingAgainst}</p> : null}
{Preview ? <p>Preview: {Preview}</p> : null}
{Verdict ? <p>Verdict: {Verdict}</p> : null}
{Port ? <p>Port: {Port}</p> : null}
{Product ? <p>Product: {Product}</p> : null}
{Version ? <p>Version: {Version}</p> : null}
{Expires ? <p>Expires: {Expires}</p> : null}
<div>
<a type="button" href={Url || URL || Shodan_URL} className="btn btn-dark btn-sm my-1">Go to Source</a>
<button onClick={onDelete} className="btn btn-danger btn-sm my-1">Delete</button>
</div>
</div>
)
}
Here is the file with my state:
const EventState = props => {
const initialState = {
events: [],
loading: false,
}
const [state, dispatch] = useReducer(EventReducer, initialState);
// Get Events
const getEvents = async () => {
setLoading();
const res = await axios.get("http://localhost:5000/events");
dispatch({
type: GET_EVENTS,
payload: res.data
})
};
// Clear events
const clearEvents = () => {
dispatch({ type: CLEAR_EVENTS });
};
// Delete Event
const deleteEvent = async id => {
await axios.delete(`http://localhost:5000/events/${id}`);
dispatch({
type: DELETE_EVENT,
payload: id
})
};
const setLoading = () => {
dispatch({ type: SET_LOADING });
}
return <EventContext.Provider
value={{
events: state.events,
loading: state.loading,
getEvents,
clearEvents,
deleteEvent,
}}
>
{props.children}
</EventContext.Provider>
}
export default EventState;
Here is where the state passes the deletion of the event to the reducer:
import {
GET_EVENTS,
CLEAR_EVENTS,
DELETE_EVENT,
SET_LOADING
} from '../types';
export default (state, action) => {
switch (action.type) {
case GET_EVENTS:
return {
...state,
events: action.payload,
loading: false,
};
case SET_LOADING:
return {
...state,
loading: true
};
case CLEAR_EVENTS:
return {
...state,
events: [],
loading: false
};
case DELETE_EVENT:
return {
...state,
events: state.events.filter(event => event._id !== action.payload)
};
default:
return state;
}
}
The item is removed from the database, but not from the front end. Any thoughts?

Related

Changing value to true onClick on object in react redux

I have a simple todo app in which i add my "todos" and if they are done i just simply click done. Although after clicking the state is updated and the payload is being printed to the console with proper actions "TODO_DONE", done field still remains false.
my case for "TODO_DONE" in Reducer:
case "TODO_DONE":
return state.map((todo) => {
if (todo.id === action.payload) {
return {
...todo,
done: true,
};
}
return todo;
});
i use it here:
<button onClick={() => doneTodo(todo)}>Done</button>
in the TodoList component:
import { deleteTodoAction, doneTodo } from "../actions/TodoActions";
import { connect } from "react-redux";
const TodoList = ({ todoss, deleteTodoAction, doneTodo }, props) => {
return (
<div>
<h3>Director list</h3>
{todoss.map((todo) => {
return (
<div>
<div> {todo.name} </div>
<button onClick={() => deleteTodoAction(todo)}>UsuĊ„</button>
<button onClick={() => doneTodo(todo)}>Done</button>
</div>
);
})}
</div>
);
};
const mapStateToProps = (state) => {
return {
todoss: state.todoss,
};
};
const mapDispatchToProps = {
deleteTodoAction,
doneTodo,
};
export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
Ofc, the "done" value is my initial value inside TodoForm with Formik:
<Formik
initialValues={{
id: uuidv4(),
name: "",
date: "",
done: false,
}}
onSubmit={(values) => handleSubmit(values)}
enableReinitialize={true}
>
Anyone knows why this doest not work?
Check your doneTodo action. Since you are passing todo object to it. It should be action.payload.id instead of action.payload.
todo.id === action.payload.id

Redux - Arrays in my reducers not showing on first load and I'm receiving errors

I'm new to Redux and React and I'm struggling with an issue for a while now. I have a reducer which contains a favorites array and another array storing the id's of the favorite movies.
Everything seemed to be working at first, but
the problem is when I open the app in another browser I receive error of *cannot read property filter / find of undefined, I get an error everywhere where I try to filter arrays of my reducer. What should I do differently ? I need to filter them because I want my buttons text to be 'Added' for the movies already in the watchlist. Is there something wrong with my reducer or is there another method I could use for filtering?
const intitialState = {
favorites: [],
favIds: [],
btnId: [],
isFavorite: false,
toggle: false,
};
const watchlistReducer = (state = intitialState, action) => {
console.log(action, state);
switch (action.type) {
case 'ADD_WATCHLIST':
return {
...state,
favorites: [...state.favorites, action.payload],
favIds: [
...state.favorites.map((movie) => movie.id),
action.payload.id,
],
btnId: action.payload.id,
isFavorite: true,
toggle: true,
};
case 'REMOVE_WATCHLIST': {
return {
...state,
favorites: [
...state.favorites.filter((movie) => movie.id !== action.payload.id),
],
favIds: [...state.favorites.map((movie) => movie.id)],
btnId: action.payload.id,
isFavorite: false,
toggle: false,
};
}
case 'CLEAR_ALL': {
return intitialState;
}
default:
return state;
}
};
export default watchlistReducer;
const MovieDetail = () => {
const dispatch = useDispatch();
const { movie, isLoading } = useSelector((state) => state.detail);
const { favorites, favIds } = useSelector((state) => state.watchlist);
const [addFav, setAddFav] = useState([favorites]);
//Toggle
const [btnText, setBtnText] = useState('Add to Watchlist');
const [isToggled, setIsToggled] = useLocalStorage('toggled', false);
const [selected, setSelected] = useState([favIds]);
const history = useHistory();
const exitDetailHandler = (e) => {
const element = e.target;
if (element.classList.contains('shadow')) {
document.body.style.overflow = 'auto';
history.push('/');
}
};
const shorten = (text, max) => {
return text && text.length > max
? text.slice(0, max).split(' ').slice(0, -1).join(' ')
: text;
};
const toggleClass = () => {
setIsToggled(!isToggled);
};
const changeText = (id) => {
const check = favIds.filter((id) => id === movie.id);
check.includes(id) ? setBtnText('Added') : setBtnText('Add to Watchlist');
};
const addFavHandler = (movie) => {
const checkMovie = favorites.find((fav) => fav.id === movie.id);
if (!checkMovie) {
dispatch(addWatchlist(movie));
setAddFav([...addFav, movie]);
setBtnText('Added');
} else if (checkMovie) {
alert('Remove from Watchlist?');
dispatch(removeWatchlist(movie));
setBtnText('Add to Watchlist');
} else {
setAddFav([...addFav]);
}
};
return (
<>
{!isLoading && (
<StyledCard>
<StyledDetails
onChange={() => changeText(movie.id)}
className='shadow'
style={{
backgroundImage: ` url("${bgPath}${movie.backdrop_path}")`,
backGroundPosition: 'center center',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundBlendMode: 'multiply',
}}
onClick={exitDetailHandler}
>
<StyledInfo>
<StyledMedia>
<img
src={
movie.poster_path || movie.backdrop_path
? `${imgPath}${movie.poster_path}`
: unavailable
}
alt='movie'
/>
</StyledMedia>
<StyledDescription>
<div className='genre'>
<h3>{movie.title}</h3>
<p>
{movie.genres &&
movie.genres.map((genre) => (
<span key={genre.id}>{genre.name}</span>
))}
</p>
</div>
<div className='stats'>
<p>
Release Date: <br />{' '}
<span>
{' '}
{movie.release_date ? movie.release_date : ` N / A`}{' '}
</span>
</p>
<p>
Rating: <br /> <span> {movie.vote_average} </span>
</p>
<p>
Runtime: <br /> <span>{movie.runtime} min</span>
</p>
</div>
<div className='synopsys'>
<p>
{shorten(`${movie.overview}`, 260)}
...
</p>
</div>
<button
className={btnText === 'Added' ? 'added' : 'btn'}
id={movie.id}
key={movie.id}
value={movie.id}
type='submit'
onClick={() => {
toggleClass();
addFavHandler(movie);
}}
>
{btnText}
</button>
</StyledDescription>
</StyledInfo>
</StyledDetails>
</StyledCard>
)}
</>
);
};

React - Redux; .map function duplicates Array sometimes

I have a .map function in a component:
let recentItemsMarkup = loading ? (
<p>Items are loading...</p>
) : (
items.map(item => (
<ShoppingItem
key={item._id}
id={item._id}
name={item.name}
createdAt={item.date}
/>
))
);
When I post an item, sometimes -not always- it duplicates on the view, but not in Database. DB is working properly, but somehow, after I post an item, it is not always sets the items properly,
here are the action and the reducer:
//post item
export const postItem = newItem => dispatch => {
dispatch({ type: LOADING_UI });
axios
.post("http://localhost:5000/api/items", newItem)
.then(res => {
dispatch({
type: POST_ITEM,
payload: res.data
});
})
.catch(err => {
dispatch({
type: SET_ERRORS,
payload: err.response.data
});
});
};
and the reducer:
const initialState = {
items: [],
item: {},
loading: false
};
export default (state = initialState, action) => {
switch (action.type) {
case LOADING_ITEMS:
return {
...state,
loading: true
};
case GET_ITEMS:
return {
...state,
items: action.payload,
loading: false
};
case POST_ITEM:
return {
...state,
items: [action.payload, ...state.items]
};
case DELETE_ITEM:
return {
...state,
items: state.items.filter(item => item._id !== action.payload)
};
default:
return state;
}
};
I checked the Ids and Database, everything is ok, ids are unique vs. Why this happening?
screenshot
and also Shopping Item component:
class ShoppingItem extends Component {
render() {
const { authenticated } = this.props.user;
const { name, createdAt, classes, id } = this.props;
const deleteButton = authenticated ? (
<DeleteItem id={id} />
) : null;
return (
<Card className={classes.card}>
<CardContent>
<Typography variant="body1" color="textPrimary">
{this.props.id}
</Typography>
<Typography variant="body1" color="textPrimary">
{name}
</Typography>
{deleteButton}
<Typography color="textSecondary">
{dayjs(createdAt).format("h:mm a, MMM DD YYYY")}
</Typography>
</CardContent>
</Card>
);
}
}
ShoppingItem.propTypes = {
name: PropTypes.string.isRequired,
};
const mapStateToProps = state => ({
user: state.user
});
const actionsToProps = {
deleteItem
};
export default connect(
mapStateToProps,
actionsToProps
)(withStyles(styles)(ShoppingItem));
It seems like your backend returns an array of items together with the new one, so in that case you'd just set them on the state, instead of adding to existing items:
case POST_ITEM:
return {
...state,
items: action.payload
};
It is because of
case POST_ITEM:
return {
...state,
items: [action.payload, ...state.items]
};
just send the item which needs to add & then handle insertion at backend.
Ok, I solved the problem,
My failure is, I added "getItems" to "postItem" function which causes duplication because when I post an item it already refreshes the page and loads the Items from componentDidMount method. So it seems I didn't understand the logic very well, which is when a state or props change, the page refreshes automatically.

Redux app: Cannot read property 'filter' of undefined

I have error in my reducer:
props actual- this is the time the airplane departure or arrival, this property is located in my API
API has this structure:
{"body":{
"departure":[{actual: value},{term: value}],
"arrival":[{....},{......}]}
}
code:
airplanes.js(reducer)
import { searchFilter } from "../components/app";
export function reducer(state = {}, action) {
switch (action.type) {
case "SET_SHIFT":
return Object.assign({}, state, {
shift: action.shift
});
case "SET_SEARCH":
return Object.assign({}, state, {
search: action.search.toLowerCase()
});
case "RUN_FILTER":
var newData = state.data[action.shift].filter(x => {
return (
x.actual &&
x.actual.includes(
state.day
.split("-")
.reverse()
.join("-")
)
);
});
return Object.assign({}, state, {
shift: action.shift || state.shift,
search: action.search || state.search,
filteredData: searchFilter(state.search, newData)
});
case "LOAD_DATA_START":
return Object.assign({}, state, {
day: action.day
});
case "LOAD_DATA_END":
var newData = action.payload.data[state.shift].filter(x => {
return (
x.actual &&
x.actual.includes(
action.payload.day
.split("-")
.reverse()
.join("-")
)
);
});
return Object.assign({}, state, {
data: action.payload.data,
shift: Object.keys(action.payload.data)[0],
filteredData: searchFilter(state.search, newData)
});
default:
return state;
}
}
app.js(main component)
import React from "react";
import { Component } from "react";
import { connect } from "react-redux";
import { fetchData } from "../actions";
import TableData from "./TableData";
import TableSearch from "./TableSearch";
import Header from "./Header";
import Footer from "./Footer";
import "./app.css";
export function searchFilter(search, data) {
return data.filter(n => n.term.toLowerCase().includes(search));
}
const days = ["23-08-2019", "24-08-2019", "25-08-2019"];
class Root extends React.Component {
componentDidMount() {
this.props.onFetchData(days[this.props.propReducer.day]);
}
render() {
const { onFilter, onSetSearch, onFetchData } = this.props;
const { search, shift, data, filteredData } = this.props.propReducer;
console.log(filteredData);
return (
<div>
<Header/>
<h1>SEARCH FLIGHT</h1>
<TableSearch
value={search}
onChange={e => onSetSearch(e.target.value)}
onSearch={() => onFilter()}
/>
{days &&
days.map((day, i) => (
<button
key={day}
onClick={() => onFetchData(day)}
className={i === day ? "active" : ""}
>
{day}
</button>
))}
<br />
<div className="buttonShift">
{data &&
Object.keys(data).map(n => (
<button
data-shift={n}
onClick={e => onFilter({ shift: e.target.dataset.shift })}
className={n === shift ? "active" : "noActive"}
>
{n}
</button>
))}
</div>
{data && <TableData data={filteredData} />}
<Footer/>
</div>
);
}
}
export const ConnectedRoot = connect(
state => state,
dispatch => ({
onFilter: args => dispatch({ type: "RUN_FILTER", ...args }),
onSetSearch: search => dispatch({ type: "SET_SEARCH", search }),
onFetchData: day => dispatch(fetchData(day))
})
)(Root);
by the way the function searchFilter is written property term and not property actual. Maybe the problem is partly due to this, but not only this, because I tried to replace the term with actual, but the error remained.
How to fix this error?
Given onFilter action creator in app.js:
onFilter: args => dispatch({ type: "RUN_FILTER", ...args })
When called without the first argument:
<TableSearch ... onSearch={() => onFilter()} />
Then in airplanes.js reducer, action.shift is undefined, so state.data[action.shift] is undefined too and state.data[action.shift].filter is a TypeError.
To fix, you can use a default value:
state.data[action.shift || state.shift].filter
However, there is an additional problem, inside TableSearch.js you call:
<button ... onClick={() => onSearch(value)}>
So you shouldn't ignore the value in app.js:
<TableSearch ... onSearch={(value) => onFilter({search: value})} />
...and use action.search || state.search inside your .filter(...) depending on the business logic.

React not rerendering after action

I have an application that has a dashboard with a list of soups. Every soup has the ability to be a daily soup. So each soup has a button that if clicked, triggers an action to update my MongoDB to make the soup a daily soup. When a soup is a daily soup, it then has 3 buttons: Remove, Low, Out. If any of these buttons are clicked they trigger an action to update my MongoDB to update that particular soup. The issue I have is that when any of these buttons are clicked, it performs the action but it is not re-rendered on the screen. I have to manually refresh the page to see that it actually worked.
Note: I am using reduxThunk to immediately dispatch the action (see code below)
I have tried using
Object.assign({}, state, action.payload)
in my reducer to be sure to avoid changing the state directly.
I also tried rewriting my reducer with:
case "UPDATE_SOUP":
return {
...state,
isDaily: action.payload.isDaily,
isLow: action.payload.isLow,
isOut: action.payload.isOut
};
React Soup Component:
class Soup extends Component {
render() {
const { soup } = this.props;
return (
<div>
<div key={soup.name} className="card">
<div
className={`card-header ${
soup.isDaily ? "alert alert-primary" : null
}`}
>
{soup.isDaily ? (
<span className="badge badge-primary badge-pill">Daily Soup</span>
) : (
"Soup"
)}
</div>
<div className="card-body">
<h5 className="card-title">{soup.name}</h5>
<p className="card-text">
{soup.isLow ? (
<span className="badge badge-warning badge-pill">
This soup is marked as LOW.
</span>
) : null}
{soup.isOut ? (
<span className="badge badge-dark badge-pill">
This soup is marked as OUT.
</span>
) : null}
</p>
{soup.isDaily ? (
<div>
<button
onClick={() =>
this.props.updateSoup(soup._id, {
isDaily: false,
isLow: false,
isOut: false
})
}
className="btn btn-danger "
>
Remove
</button>
<button
onClick={() =>
this.props.updateSoup(soup._id, {
isLow: true
})
}
className="btn btn-warning"
>
Getting Low
</button>
<button
onClick={() =>
this.props.updateSoup(soup._id, {
isOut: true
})
}
className="btn btn-dark"
>
Ran Out
</button>
</div>
) : (
<button
onClick={event =>
this.props.updateSoup(soup._id, {
isDaily: true
})
}
className="btn btn-primary"
>
Make Daily
</button>
)}
</div>
</div>
</div>
);
}
}
function mapStateToProps({ soupsReducer }) {
return { soupsReducer };
}
export default connect(
mapStateToProps,
actions
)(Soup);
React SoupList Component (To show all Soups):
class SoupList extends Component {
componentDidMount() {
this.props.allSoups();
}
renderSoup() {
const { soupsReducer } = this.props;
if (soupsReducer.length > 0) {
return soupsReducer.map(soup => {
if (soup.name !== "date") {
return <Soup key={soup._id} soup={soup} />;
} else {
return null;
}
});
}
}
render() {
console.log("SoupListProps=", this.props);
return <div>{this.renderSoup()}</div>;
}
}
function mapStateToProps({ soupsReducer, dateReducer }) {
return { soupsReducer, dateReducer };
}
export default connect(
mapStateToProps,
actions
)(SoupList);
Action:
export const updateSoup = (id, update) => async dispatch => {
const res = await axios.put(`/api/allsoups/${id}`, update);
dispatch({ type: "UPDATE_SOUP", payload: res.data });
};
Reducer:
export default function(state = [], action) {
switch (action.type) {
case "FETCH_SOUPS":
return action.payload;
case "ALL_SOUPS":
return action.payload;
case "UPDATE_SOUP":
return action.payload;
default:
return state;
}
}
The issue is that you are re-writing your whole state in every action by doing
return action.payload;
You need to do something like
return { ...state, someStateKey: action.payload.data.someKey }
Where depending on the action type you pull the required data from the response and set that in your state.
If you can provide more info on the response, I can update the answer with more specific details
My thoughts are revolving around this part of your code...
export const updateSoup = (id, update) => async dispatch => {
const res = await axios.put(`/api/allsoups/${id}`, update);
dispatch({ type: "UPDATE_SOUP", payload: res.data });
};
export default function(state = [], action) {
// ...code...
case "UPDATE_SOUP":
return action.payload;
// ...code...
}
}
Try this:
Identify the souptype AND the change to your action...
dispatch({ type: "UPDATE_SOUP", payload: res.data, souptype: id, update: update });
Update the state to the souptype to your reducer...
export default function(state = [], action) {
case "UPDATE_SOUP":
const newstate = action.payload;
neswstate.soups[action.souptype] = action.isDaily ? true : false;
return newstate;
Of course, why won't this work? Simply because I'm guessing what kind of state you have and how the soups are stored in this state. There is no constructor or state definition in your code, so, you'll need to adjust what's above to match how your state is defined.

Categories