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.
Related
I'm using React Redux and want to be able to change the title and description of a post, using the onChange method. When only using React the way you would do this is that you keep an useState which you change whenever a change occurs, but I can't seem to get it to work with using redux in react. Instead of the state changing the original title, and description remains and cannot be changed.
From what I have read the basic idea is to have a listener on the input (onChange, usually) and have that fire a redux action. You then have the action tell the reducer to make the change to the store.
I have tried doing this, but could make it work correctly. What am I doing wrong and how do you solve it? I'm also wondering how do I specify that I want to change either title or description when using onChange, or do I simply send everything in post each time a change occurs?
This is what the redux state looks like when entering a post:
{
auth: {
isSignedIn: true,
user: {
id: '624481f22566374c138cf974',
username: 'obiwan',}
},
posts: {
'62448632b87b223847eaafde': {
_id: '62448632b87b223847eaafde',
title: 'hellothere',
desc: 'its been a long time since I heard that name...',
username: 'vorbrodt',
email: 'example#gmail.com',
categories: [],
createdAt: '2022-03-30T16:32:50.158Z',
updatedAt: '2022-03-30T16:32:50.158Z',
__v: 0
}
},
}
Here is where the onChange happens.
Post.js
import { getPostById, editPost } from "../actions";
const Post = ({ getPostById, editPost, username }) => {
const [updateMode, setUpdateMode] = useState(false);
let { id } = useParams();
let post = useSelector((state) => state.posts[id]);
const handleInputChange = (e) => {
try {
editPost(e.target.value);
} catch (err) {}
};
return (
<div className="post">
<div className="post-wrapper">
{updateMode ? (
<input
type="text"
value={post.title}
className="post-title-input"
autoFocus
onChange={(e) => handleInputChange(e)}
/>
) : (
<h1 className="post-title">
{post.title}
</h1>
)}
<div className="desc-area">
{updateMode ? (
<textarea
className="post-desc-input"
value={post.desc}
onChange={(e) => handleInputChange(e)}
/>
) : (
<p className="post-desc">{post.desc}</p>
)}
</div>
</div>
</div>
);
};
const mapStateToProps = (state) => {
return { username: state.auth.user.username };
};
export default connect(mapStateToProps, { getPostById, editPost })(Post);
Here is the action creator:
//edit post in redux state
const editPost = (postValues) => (dispatch) => {
dispatch({ type: EDIT_POST, payload: postValues });
};
And here is the reducer which is suppose to change the state.
postReducer.js
import _ from "lodash";
import { GET_POSTS, GET_POST, CREATE_POST, EDIT_POST } from "../actions/types";
function postReducer(state = {}, action) {
switch (action.type) {
case GET_POSTS:
return { ...state, ..._.mapKeys(action.payload, "_id") };
case GET_POST:
return { ...state, [action.payload._id]: action.payload };
case CREATE_POST:
return { ...state, [action.payload._id]: action.payload };
case EDIT_POST:
//here the change should occur, not sure how to specify if title or desc should
//change
return { ...state, [action.payload._id]: action.payload };
default:
return state;
}
}
export default postReducer;
Hey there something like this should be of help
const handleInputChange = (e, key, id) => {
try {
editPost({ [key]: e.target.value, id });
} catch (err) {}
};
Usage
<textarea
className="post-desc-input"
value={post.desc}
onChange={(e) => handleInputChange(e, "title", post.id)}
/>
action
const editPost = (postValues) => (dispatch) => {
dispatch({ type: EDIT_POST, payload: postValues });
};
Reducer
case EDIT_POST:
//here we destructure the id and return the data without the id cause we //need it below
const {id, ...newData} = action.payload
const indexToUpdate = state.posts.find(post => post.id === id)
const newPostsData = [...state.posts]
//Here we update the actual object and its property that is in the state at //the specific value
newPostsData[indexToUpdate] = {...newPostData[indexToUpdate], {...newData}
return { ...state, posts: newPostsData};
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
I am making a react-redux site.
I am accessing data called from an api via redux.
I understand that ComponentDidMount will not wait for this data to be called so I was wondering on a better way to split this data within a parent component into arrays for children components (or if this method is a bad choice).
This is the component and will hopefully shed some light on what is going on.
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { fetchPosts, fetchItins } from "../../../actions/postActions";
import TravelAlerts from "./TravelAlerts//travelAlert";
import IntelligenceAlerts from "./IntelligenceAlerts/IntelligenceAlert";
import AllAlerts from "./AllAlerts/AllAlerts";
class Alerts extends Component {
state = {
showAllAlerts: true,
showAllIntelligenceAlerts: false,
showAllTravellers: false,
currentPage: 1,
alertsPerPage: 20,
travelAlerts: [],
intelligenceAlerts: [],
};
componentDidMount() {
this.props.fetchPosts();
console.log(this.props.posts);
for (var key in this.props.posts) {
if (this.props.posts.hasOwnProperty(key)) {
if (key === "travelAlerts") {
alert("travel ALerts Hit");
} else if (key === "intelligenceAlerts") {
alert("intelligenceAlertsHIts");
} else {
}
console.log(key + " -> " + this.props.posts[key]);
}
}
}
//navigation helper
DisableAlerts() {
this.setState({
showAllAlerts: false,
showAllIntelligenceAlerts: false,
showAllTravellers: false,
});
}
//pagination change page
handleClick(number) {
this.setState({
currentPage: number,
});
}
ToogleAlertType(name) {
this.DisableAlerts();
if (name === "All") {
this.setState({ showAllAlerts: true });
} else if (name === "Intelligence") {
this.setState({ showAllIntelligenceAlerts: true });
} else if (name === "Travellers") {
this.setState({ showAllTravellers: true });
} else {
this.setState({ showAllAlerts: true });
}
}
render() {
return (
<div>
<button
style={{ width: "30%" }}
onClick={() => this.ToogleAlertType("ALL")}
>
ALL{" "}
</button>
<button
style={{ width: "30%" }}
onClick={() => this.ToogleAlertType("Intelligence")}
>
Intelligence{" "}
</button>
<button
style={{ width: "30%" }}
onClick={() => this.ToogleAlertType("Travellers")}
>
Travellers
</button>
<br />
<hr />
<div>
{this.state.showAllAlerts ? (
<>{/* <AllAlerts alerts={this.props.posts} /> */}</>
) : (
<></>
)}
</div>
<>
{this.state.showAllTravellers ? (
<>
<></>
{/* <TravelAlerts alerts={this.props.posts} /> */}
</>
) : (
<></>
)}
</>
<>
{this.state.showAllIntelligenceAlerts ? (
<>{/* <IntelligenceAlerts alerts ={this.props.posts}/> */}</>
) : (
<></>
)}
</>
</div>
);
}
}
Alerts.propTypes = {
fetchPosts: PropTypes.func.isRequired,
posts: PropTypes.object.isRequired,
// newPost: PropTypes.object
};
const mapStateToProps = (state) => ({
posts: state.posts.items,
// newPost: state.posts.item
});
export default connect(mapStateToProps, { fetchPosts })(Alerts);
The component is mapped to redux and is working fine however the data being retrieved is within an object and I would like this to be two separate arrays which I could then pass down to the child components etc.
This is what I am currently trying to do in the component did mount just to see if it can find the keys.
componentDidMount() {
this.props.fetchPosts();
console.log(this.props.posts);
for (var key in this.props.posts) {
if (this.props.posts.hasOwnProperty(key)) {
if (key === "travelAlerts") {
alert("travel ALerts Hit");
} else if (key === "intelligenceAlerts") {
alert("intelligenceAlertsHIts");
} else {
}
console.log(key + " -> " + this.props.posts[key]);
}
}
}
However the data does not show when mounted(works in render method but I feel that is not a good place to put it).
Is this a good direction to head in or should I have split these into two separate arrays before they have even reached the component? If so should this be done so in the reducer or in the actions?
Really appreciate any help as I am new to redux.
EDIT
This is my fetch posts
export const fetchPosts = () => (dispatch) => {
fetch(
"the url im using"
)
.then((res) => res.json())
.then((posts) =>
dispatch({
type: FETCH_POSTS,
payload: posts,
})
);
};
My reducer
import { FETCH_POSTS, NEW_POST, FETCH_ITINS } from "../actions/types";
const initialState = {
items: {},
item: {},
itins: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case FETCH_POSTS:
return {
...state,
items: action.payload,
};
case NEW_POST:
return {
...state,
item: action.payload,
};
case FETCH_ITINS:
return {
...state,
itins: action.payload,
};
default:
return state;
}
}
I am creating a list inside the Component file dynamically by mapping data from a JSON response with the help of thunk middleware.
I want to select elements from that list and add them to "My Favorites",
My JSON response doesn't have a unique ID but a unique string.
Reducer:
const initialstate = { isFav: false }
const reducer = (state=initialstate, action) => {
switch(action.type){
case actionTypes.TOGGLE_FAVORITE:
return {
...state,
isFav: **What to do here?**
}
default:
break;
}
}
export default reducer;
Action.js:
export const TOGGLE_FAVORITE = 'TOGGLE_FAVORITE';
export const togglefav = (url) =>{
return{
type: TOGGLE_FAVORITE,
payload: url
}
}
Component.js
this.props.dailySource.map((source,index) =>{
...
<div className={classes.star}>
<span className="fa fa-star" key={index} onClick={()=>
this.props.onToggleFavorite(source.url) }
style={{ color: (this.props.toggleFav) ? 'red' : '' }}></span>
</div>
}
}))
const mapStateToProps = state =>{
return{
dailySource: state.list.newsItem,
toggleFav: state.list.isFav
}
}
const mapDispatchToProps = dispatch =>{
return{
onToggleFavorite: (url) => dispatch (actionCreators.togglefav(url))
}
}
It will be better if you can simplify your pattern.
Instead of extracting isFav to another state tree, it would be much easier if you put it inside the objects of the list itself.
Let's see the code.
Reducer
const initialState = [];
const reducer = (state=initialState, action) => {
switch(action.type) {
case actionTypes.TOGGLE_FAVORITE:
const dailySource = [...state].map((v) => {
if (v.url === action.url) {
v.isFav = true;
}
return v;
});
return dailySource;
default:
return state;
}
}
Component
this.props.dailySource.map((source,index) =>{
<div className={classes.star}>
<span className="fa fa-star" key={index} onClick={()=>
this.props.onToggleFavorite(source.url) }
style={{ color: source.isFav ? 'red' : '' }}></span>
</div>
}
}))
I'm currently building my first app with React and today I encountered a problem I think I cannot resolve on my own yet.
So what's the matter:
I'm rendering a Result.js container, which consists of smaller components displaying data from the API. Initially, it is hidden (not rendered) and it gets rendered after passing query into Search component and receiving a response. I'm trying to implement a transition so it fades in (opacity 0 -> 1) after response. This is working fine, but also I want it to fade out when the user sends another request and fades in again. This is what's not working, or working parts. Right now the fade-out animation plays out, but right near the end, there is a flash of an earlier state of the component with previous data. Like there was an additional render in there. I tried different approaches like with inline styling (display: none) but most of them ended with fade-out animation not playing at all.
I'm using Redux to store API response and components' display property.
The code I've been working on can be found below. I'll be very thankful for any suggestions or insights, also related to my coding style/code 'cleanness' :) Thank you!
Result.js container:
const Result = props => {
return (
<Transition
in={props.displayResult}
timeout={1000}
mountOnEnter
unmountOnExit
>
{state => (
<div
className={`${classes.Box} ${
state === 'entering'
? classes.ResultOpen
: state === 'entered'
? classes.ResultVisible
: state === 'exiting'
? classes.ResultClosed
: state === 'exited'
? classes.ResultVisible
: null
}`}
>
<div className={classes.BoxRow}>
<Sprites />
<NameId />
</div>
<div className={classes.BoxRow}>
<div className={classes.BoxColumn}>
<Abilities />
<Metrics />
</div>
<Types />
</div>
<div className={classes.BoxRow}>
<Stats />
</div>
</div>
)}
</Transition>
);
};
const mapStateToProps = state => {
return {
displayResult: state.result.displayResult
};
};
export default connect(mapStateToProps)(React.memo(Result));
reducer.js
const initialState = {
id: null,
name: '',
spriteFront: '',
spriteBack: '',
types: [],
height: null,
weight: null,
stats: [],
baseExperience: null,
abilities: [],
moves: [],
displayResult: false,
error: false,
loading: false
};
const setResult = (state, action) => {
return updateObject(state, {
id: action.result.id,
name: action.result.name,
spriteFront: action.result.sprites.front_default,
spriteBack: action.result.sprites.back_default,
types: action.result.types,
height: action.result.height,
weight: action.result.weight,
stats: action.result.stats,
baseExperience: action.result.base_experience,
abilities: action.result.abilities,
moves: action.result.moves,
displayResult: true
});
};
const resetBox = (state, action) => {
return updateObject(state, {
displayResult: false
});
};
const fetchResultFailed = (state, action) => {
return updateObject(state, { error: true });
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.SET_RESULT:
return setResult(state, action);
case actionTypes.FETCH_RESULT_FAILED:
return fetchResultFailed(state, action);
case actionTypes.RESET_BOX:
return resetBox(state, action);
default:
return state;
}
};
export default reducer;
actions.js
export const setResult = result => {
return {
type: actionTypes.SET_RESULT,
result: result
};
};
export const resetBox = () => {
return {
type: actionTypes.RESET_BOX
};
};
export const fetchResultFailed = () => {
return {
type: actionTypes.FETCH_RESULT_FAILED
};
};
export const nextResult = query => {
return dispatch => {
dispatch(resetBox());
setTimeout(() => {
dispatch(initResult(query));
}, 100);
};
};
export const initResult = query => {
return dispatch => {
axios
.get(`https://pokeapi.co/api/v2/pokemon/${query}`)
.then(response => {
dispatch(setResult(response.data));
console.log(response.data);
})
.catch(error => {
dispatch(fetchResultFailed());
});
};
};