I have a modal containing a button that fires a HTTP request, at which point the displayed html will change depending on a successful/error response from the server, where the response changes a state prop that is dealt with in the mapStatesToProps function.
The issue I have now is that I am wanting to reset the modal to its initial state pre-request when I close it.
I had previously done this by using local component state but have since updated the functionality to use the request mapped state props shown above.
I am curious if it possible to reset the state without firing a dispatch to a random URI?
Component.jsx
const mapStatesToProps = ({myState}) => ({
response: myState.response,
success: !!(myState.success),
fail: !!(myState.fail)
});
const mapDispatchToProps = dispatch => ({
doReq: () => {
dispatch(doMyRequest());
}
});
class MyComponent extends Component {
toggleModal = () => // modal toggle code
render() {
const {response, success, fail} = this.props;
<div className="myModal">
// Modal stuff here
{!success && !fail && (
<button onClick="() => toggleModal()">Close modal</button>
)}
{success && !fail && (
<h1>Some success message</h1>
)}
{!success && fail && (
<h1>Some fail message</h1>
)}
</div>
}
}
req-actions.js
export const MY_REQUEST;
export const MY_REQUEST_SUCCESS;
export const MY_REQUEST_ERROR;
export const doMyRequest = () => ({
type: MY_REQUEST,
agent: agent.req.doRequest
})
req-reducer.js
import { deepEqual, deepClone } from '../McUtils';
import {
MY_REQUEST,
MY_REQUEST_ERROR,
MY_REQUEST_SUCCESS
} from "../actions/req-actions";
export default (state = {}, action) => {
let newState = deepClone(state);
switch (action.type) {
case MY_REQUEST:
console.log('SENDING REQUEST');
newState.response = null;
newState.success = false;
newState.fail = false;
break;
case MY_REQUEST_SUCCESS:
console.log('SUCCESS');
newState.response = action.payload;
newState.success = true;
newState.fail = false;
break;
case MY_REQUEST_ERROR:
console.log('FAIL');
newState.response = action.payload;
newState.success = false;
newState.fail = true;
break;
default:
return state;
}
return newState;
}
Just use another action:
case MY_REQUEST_RESET:
return {} // only putting {} in here because this is what you have defined your initialState to be according to your reducer.
Personal preference is to clearly define your initial state like this.
const initialState = {};
export default (state = initialState, action) => {
switch(action.type) {
... your existing handlers
case MY_REQUEST_RESET:
return initialState
}
}
Wiring it up:
const mapDispatchToProps = dispatch => ({
doReq: () => {
dispatch(doMyRequest()),
},
reset: () => {
dispatch(resetMyRequest());
}
});
// types
const MY_REQUEST_RESET = 'MY_REQUEST_RESET';
// action creator (may be referred to as "actions")
const resetMyRequest = () => ({ type: MY_REQUEST_RESET })
EDIT: While I'm here, this is really gross:
let newState = deepClone(state);
and reeks of "I don't really know what I'm doing" and can lead to performance issues. You are deepCloning the state on every action fired through redux, even if the actions aren't one's this reducer is interested in.
If you are changing the state in the reducer, just change the part you are concerned with, don't change "all" of it.
e.g.
export default (state = {}, action) => {
switch (action.type) {
case MY_REQUEST:
console.log('SENDING REQUEST');
return {
success: false,
fail: false,
response: null
}
case MY_REQUEST_SUCCESS:
console.log('SUCCESS');
return {
...state, // this will contain "fail: false" already
success: true,
response: action.payload
};
case MY_REQUEST_ERROR:
console.log('FAIL');
return {
...state, // this will contain "success: false" already
error: true,
response: action.payload
};
default:
return state;
}
Related
How can I remove a specific item (by id) from localstorage using react (redux - persist)? handleSubmit is working fine, but handleDelete, is not. I have this:
handleSubmit = event => {
event.preventDefault();
this.props.addWeather(this.state.weatherCity);
this.setState({ weatherCity: "" });
};
handleDelete = (event, id) => {
this.props.deleteWeather(this.state.weatherCity);
this.setState({ weatherCity: "" });
}
const mapStateToProps = state => ({
allWeather: state.allWeather
});
const mapDispatchToProps = dispatch =>
bindActionCreators(WeatherActions, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(WeatherList);
And button in form to call handleDelete:
<form onSubmit={this.handleDelete}><button type="submit" id="add" onClick={this.handleDelete}>Remove City</button></form>
My localstorage:
allWeather: "[{\"id\":0.5927975642362653,\"city\":\"Toronto\"},{\"id\":0.8124764603718682,\"city\":\"Fortaleza\"},{\"id\":0.9699736666575081,\"city\":\"Porto\"},{\"id\":0.852871998478355,\"city\":\"Tokio\"},{\"id\":0.8854642571682461,\"city\":\"New York\"}]"
My reducer:
export default function allWeather(state = [], action) {
switch (action.type) {
case "ADD_WEATHER":
return [...state, { id: Math.random(), city: action.payload.city }];
case "DELETE_ITEM":
return [...state, state.weatherCity.filter((event, id) => id !== action.payload.id)];
default:
return state;
}
}
And actions:
export const deleteWeather = id => ({
type: "DELETE_ITEM",
payload: { id }
});
I appreciate any help.
Your problem is that you are using the spread operator, which copies the content of the current state first. Then you are adding the items that were returned from the filter method. So you aren't deleting but adding. To delete from an array use the filter method only, without the spread operator like that:
return state.filter( (city) => city.id !== action.payload.id )
Also the state is an array, not an object, so this is invalid state.weatherCity.
I just learn react-native basic, and when work with redux, i have problem with useSelector , here is some of my code
Here is store component
//store.js
initState = {
loginStatus: false,
}
const LoginAction = (state = {initState}, action) => {
if (action.type == 'changeLogin') {
return { loginStatus:!state.loginStatus }
}
return state
}
const store = createStore(LoginAction, composeWithDevTools());
export default store
Here is Login Function
function LoginScreen({ navigation, props }) {
const dispatch = useDispatch()
const Login = useSelector(state => {
return state.LoginStatus
})
function getLogin() {
return Login
}
function handleLogin() {
dispatch({ type: 'changeLogin' })
}
console.log('Login ' + Login) // it return undefined
I have tried this method useSelector state returns undefined (React-Redux) but it didn't work!
Here is screenshot of what happened
But when i add that to login button, it return true, then continute to undefined
<Formik
validateOnMount
validationSchema={loginValidationSchema}
initialValues={{ email: '', password: '' }}
onSubmit={
() => {
handleLogin()
console.log('When submit ' + Login) // true then undefined
SetTimer();
}
// () => navigation.navigate('Login')
}
>
Please help , thank a lot
The casing is wrong in your selector. It should be return state.loginStatus. Also, your LoginAction is technically a reducer, not an action.
const Login = useSelector(state => {
return state.loginStatus
})
Edit: An additional issue in the reducer is the initial state has initState as the top-level key in the object, when the intent is just for it to be assigned directly:
const LoginAction = (state = initState, action) = {
// reducer code here
}
Summary
I have 2 states: isLogged and counter. isLogged is a bool where the user can set it to true or false, in doing so the user can access certain pages where if isLogged is set to true. The counter is just a simple int where the user can increment/decrement it. Both states are saved on sessionStorage if their values are changed; and on init, I ran a dispatch to get both values from the sessionstorage. The default value for isLogged is false, and counter is 0.
The Problem
Let's say for example we have this state values:
isLogged = true
counter = 4
If I try to call an increment action, the values will now be this:
isLogged = false
counter = 5
As you can see, it resets the isLogged to its default value. However when I look on the sessionstorage panel on Chrome, it says there: {isLogged: true, counter: 5} so it is still saving the values on the sessonstorage and everything there seems to work fine. However on the app itself, it says that isLogged is now false and the user cannot access certain pages anymore even if he didn't logout for some reason.
What's weird is if I refresh the page, the states are now:
isLogged = true
counter = 5
(of course the data have been fetched from the sessionstorage) But if I now switch the isLogged to false, it will also reset the counter to 0:
isLogged = false
counter = 0
So the problem is when I call a dispatch/action to modify a certain state, all other states get reset.
Codes
src/actions/index.js
export const increment = (num) => {
return {
type: 'INCREMENT',
payload: num
}
}
export const decrement = () => {
return {
type: 'DECREMENT'
}
}
export const get_data = () => {
return {
type: 'GET'
}
}
export const signin = () => {
return {
type: 'SIGN_IN'
}
}
export const signout = () => {
return {
type: 'SIGN_OUT'
}
}
export const getlogged = () => {
return {
type: 'GET'
}
}
src/reducers/counter.js
const counterReducer = (state = 0, action) => {
switch(action.type) {
case 'INCREMENT':
state += action.payload
saveCounter(state)
return state
case 'DECREMENT':
state -= 1
saveCounter(state)
return state
case 'GET':
state = getCounter()
return state
default:
return 0
}
}
const saveCounter = (state) => {
const data = {
counter: state
}
sessionStorage.setItem("data", JSON.stringify(data))
}
const getCounter = () => {
if (sessionStorage.getItem("data") != null) {
const data = JSON.parse(sessionStorage.getItem("data"))
return data["counter"]
}
else {
return 0
}
}
export default counterReducer
src/reducers/isLogged.js
const loggedReducer = (state = false, action) => {
switch(action.type) {
case 'SIGN_IN':
state = true
saveLogged(state)
return state
case 'SIGN_OUT':
state = false
saveLogged(state)
return state
case 'GET':
state = getLogged()
return state
default:
return false
}
}
const saveLogged = (state) => {
const data = {isLogged: state}
sessionStorage.setItem("logged", JSON.stringify(data))
}
const getLogged = () => {
if (sessionStorage.getItem("logged") != null) {
const data = JSON.parse(sessionStorage.getItem("logged"))
return data["isLogged"]
}
else {
return false
}
}
export default loggedReducer
I'm calling it on a component like this:
useEffect(() => {
getCounter()
}, [])
const counter = useSelector(state => state.counter)
const dispatch = useDispatch()
const getCounter = () => {
dispatch(get_data())
}
return (
<div className="container-fluid mt-3">
<p className="text-white">Counter: {counter}</p>
<button onClick={() => dispatch(increment(4))} className="btn btn-primary">+</button>
<button onClick={() => dispatch(decrement())} className="btn btn-danger">-</button>
</div>
)
The problem is that in both your reducers, you're returning the default state in the default case. This means that every non handled action will reset the states.
For instance, when you dispatch an increment action, it goes in the default state in your reducer and sets the state to false.
The default case should simply return state unchanged.
Besides that, you're useEffect seems a bit dangerous, as getCounter changes at every render, it will be called every time.
I would also advise you to use a middleware for redux if you want to save something in the localStorage. A reducer is supposed to be a function with no side-effects so you're not respecting the rule here.
You should also not read from the localStorage via an action but instead read from the localStorage when creating your store.
I want to implement an action which gets item by id, so I've created fetchItemAction(), as follows:
export const fetchItemAction = () => (dispatch) => {
dispatch({
type: FETCH_ITEM_REQUEST,
});
return axios.get(`${url}/notes/5d4724cd62087b0e141f75a4`)
.then(({ data }) => {
console.log(data);
dispatch({
type: FETCH_ITEM_SUCCESS,
data,
});
})
.catch((error) => {
dispatch({
type: FETCH_ITEM_FAILURE,
});
});
};
Then, I try to set item field in State in my reducer:
const initialState = {
isAuthenticated: false,
user: {},
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ITEM_REQUEST:
return {
...state,
isLoading: true,
};
case FETCH_ITEM_SUCCESS:
return {
...state,
item: action.data,
isLoading: false,
};
}
};
Then, I try to get those data in Details component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchItemAction } from 'actions/actions';
class Details extends Component {
componentDidMount() {
const { fetchItem } = this.props;
fetchItem();
}
render() {
const { item, isLoading } = this.props;
return (
<>
{console.log(item)}
{/* <p>{item.title}</p> */}
</>
);
}
}
const mapStateToProps = ({ item, isLoading }) => ({ item, isLoading });
const mapDispatchToProps = dispatch => ({
fetchItem: () => dispatch(fetchItemAction()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Details);
As a result, I'm getting following data in console:
Apart from two undefinded the result looks good because there is correct response from my backend.
But, when I try to uncomment <p>item.title</p> line in Details.js, the app crash:
TypeError: Cannot read property 'title' of undefined
I also implemented correctly fetchItemsAction(), addItemAction() and deleteItemAction() which are very similar but I have no idea what is wrong in fetchItemAction().
This is an asynchronous issue. componentDidMount is called when the component is mounted. Then, you're calling fetch. So on your first render, item is undefined. Once the data is returned, the render is triggered again with item data.
So, just check if item is defined:
render() {
const { item, isLoading } = this.props;
return (
<>
{console.log(item)}
{item && <p>{item.title}</p>}
</>
);
}
I built an app and added CRUD functionality and everything works fine except the edit functionality. The problem is when I try to edit its actually hitting the right database and updates the entry but in the react app its just force updates all the entries to particular one entry.
Update Saga :-
function* updateFeedbackSaga(action) {
try {
const updateData = yield call(api.feedback.edit, action.payload);
yield put(actions.updateFeedback(updateData));
console.log(updateData);
} catch (err) {
yield put(actions.updateFeedbackErrors(err.response.data));
}
}
Edit Reducer
import * as actionTypes from "../Actions/types";
const initialState = {
feedbacks: [],
feedback: {},
loading: false
};
export default (state = initialState, action) => {
switch (action.type) {
case actionTypes.UPDATE_FEEDBACK:
return {
...state,
feedbacks: state.feedbacks.map(
feedback =>
feedback.id === action.payload.id ? action.payload : feedback
)
};
default:
return state;
}
};
Actions
//Edit and update Feedback
export const updateFeedbackRequest = newFeedbackData => ({
type: actionTypes.UPDATE_FEEDBACK_REQUEST,
payload: newFeedbackData
});
export const updateFeedback = updatedData => ({
type: actionTypes.UPDATE_FEEDBACK,
payload: updatedData
});
export const updateFeedbackErrors = errors => ({
type: actionTypes.GET_ERRORS,
payload: errors
});
That's how printing
<section className = "feedback">
<div className = "employees__table" >
<h4 className = "semi-heading" > Feedback Table < /h4>
{
FeedbackList feedbacks = {feedbacks} />
}
</div>
</section >
const mapStateToProps = state => ({
feedbackList: selectors.FeedbackSelector(state)
});
HERE ARE THE IMAGES
This is my feedbacklist
If I edit the first item then state is like this
My feedbacklist is repeating edited feedback. I don't know where i am doing wrong.
Here is my database
Here is the working code:
https://codesandbox.io/s/github/montygoldy/employee-review/tree/master/client
login: montyjatt#gmail.com
password: 12345678
do you need to loop over all feedback when you already know the updated I'd?
case actionTypes.UPDATE_FEEDBACK:
return {
...state,
feedbacks[action.payload.id]: action.payload.body
};
This will only update a single item because the ID is part of the key.
The way you have it currently the feedbacks will all be replaced by the single value that matches the ID.
If you're planning on sending multiple id's then you'll want to use the spread operator.
case actionTypes.UPDATE_FEEDBACK:
return {
...state,
feedbacks: {
...state.feedbacks,
...action.payload
}
};
In this case you're spreading out the old feedback items and then using the new payload with the spread operator to overwrite only the ones with matching id's.
Of course this means the action.payload should match your feedback structure.
Ok SO I have found the fix, actually, it's my id reference in the reducer was incorrect.
correct way is
case actionTypes.UPDATE_FEEDBACK:
return {
...state,
feedbacks: state.feedbacks.map(
feedback =>
feedback._id === action.payload._id ? action.payload : feedback
)
};