I'm learning redux for my first react-redux application. How do I manage to verify payload value before changing my state ? For example the code below:
todoExample = {name: 'learn redux', author: 'myself'}
wrongTodoExample = {name: 'learn redux'}
dispatch(addTodos({todo: todoExample}))
dispatch(addTodos({todo: wrongTodoExample }))
With the above code, I add 2 todo items to my state but they don't have the same keys.
Is there a way to check the payload value in order to authorize the first addTodos but not the second one in my reducer?
I've searched on the internet but I couldn't find an answer. I'm sorry if my question is redundant.
You can use redux middleware to verify things, that is absolutely one of the intended use cases for middleware. Any middleware can inspect and modify any action going through the pipeline before it reaches the reducers, and even prevent an action from continuing on.
const verifyPayload = store => next => action => {
if (isVerifyPayload(action.payload)) {
return next(action);
} else {
return store.dispatch({ type: 'NOT_AUTHORIZED' })
}
}
const store = createStore(
initialState,
applyMiddleware(verifyPayload)
)
Not so clear about your description about same key, you mean name or author, or other specific keys like code\id.
You can try to validate your todos before dispatch or within the addTodos
function addTodos(payload) {
if (!payload.todo.code) return;
// simply return,
// otherwise throw an error to indicate that your todos miss a specific key
}
You can use a ternary operator in your reducer along with some util function to validate your todo. If the todo is valid, then transform your state to include the new todo, if not return the same state (effectively doing nothing).
const isValidTodo = (todo) => {
//Implement your validations. E.g: A valid todo will have a name and an author
return todo.name && todo.author;
}
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return isValidTodo(action.payload) ?
[
...state,
{
name: action.payload.name,
author: action.payload.text,
completed: false
}
]
: state
default:
return state
}
}
I've found a solution that suited well my needs and it's TypeScript. Now I have Payload Type wich allow me to define keys that I need in my action.payload without any validation function.
Thanks all for your asnwers.
Related
I have several actions which use the same reducer, and instead of having a dom operation in each of those actions, I want to just add it once inside my shared reducer. I know reducers are to be pure (which the returned data still is), but is this some kind of anti-pattern or an acceptable strategy?
case APPEND_POSTS:
!payload.length &&
document.getElementById('posts-cont').classList.add('no-more-posts'); // this
const total = state.posts.length + payload.length;
const limit = total > posts_to_keep_limit ? 50 : 0;
return {
...state,
posts: [...state.posts.slice(limit), ...payload],
loading: false,
};
```
Redux Action
case APPEND_POSTS:
// you don't need to use below code.
// !payload.length && document.getElementById('posts-cont').classList.add('no-more-posts'); // this
const total = state.posts.length + payload.length;
const limit = total > posts_to_keep_limit ? 50 : 0;
return {
...state,
posts: [...state.posts.slice(limit), ...payload],
nomore: true,
loading: false,
};
Your component.
function YourComp(props){
const state = useSelector(...);
return ( <div id="posts-cont" className={state.nomore ? 'no-more-posts' : ''} > {...}</div>
}
I know reducers are to be pure (which the returned data still is), but is this some kind of anti-pattern or an acceptable strategy?
The returned data is pure, but you've introduced a side-effect in the form of a DOM mutation. Therefore, this reducer is not pure.
This is indeed an anti-pattern because now, the component(s) that render posts-cont items have an invisible coupling to this reducer. It makes your codebase more difficult to read and debug.
jinongun's advice is good: let the className of the component derive its value from the store's state using a selector. AS for the general question
I have several actions which use the same reducer, and instead of
having a dom operation in each of those actions, I want to just add it
once inside my shared reducer.
DON'T EVER make DOM operations inside a reducer.
Don't ever make any operation that is not a pure computation.
But you can create an action creator that always calls a side effect (with Redux-Thunk):
function appendPosts(payload) {
return dispatch => {
mySideEffect()
dispatch({
type: APPEND_POSTS,
payload
})
}
}
function action1(params) {
return dispatch => {
dispatch({
type: ACTION1,
payload: params
})
dispatch(appendPosts(params))
}
}
function action2(params) {
return dispatch => {
dispatch({
type: ACTION2,
payload: params
})
dispatch(appendPosts(params))
}
}
// etc
I want to know why when I dispatch action before my console log prints old state.
if I do next:
reducer.js
let initialState = { display: false };
const MyReducer = (state = initialState,action) => {
...
case 'SET_DISPLAY':
return { update(state,{ display : {$set: action.display } }) }
break;
default:
return state;
break;
}
ActionCreator.js
let ActionCreator = {
setDisplay(value) {
return(dispatch,getState) {
dispatch({ type: 'SET_DISPLAY',display: value})
}
}
};
app.js
componentDidMount(){
this.props.dispatch(ActionCreator.setDisplay(true))
// expected : true
console.log(this.props.display)
// prints : false.
}
const mapStateToProps = (state) => {
display : state.display
}
but I can see changes in my redux dev-tools console.
PD I use redux-thunk as Middleware.its just example,all my code seems good and works great,but,its a question.
Why console logs old state instead a new state (its ilogic, if I dispatched an action before call logs) I will apreciate your answers,thanks.
This is because you are using redux-thunk and your dispatch happens aynchronously.
this.props.dispatch(ActionCreator.setDisplay(true)) will not set display true immediately.
Since you are not making a network request or anything async in that action why dont you change the action creator to
let ActionCreator = {
setDisplay(value) {
return { type: 'SET_DISPLAY',display: value};
}
};
Now it will happen synchronously. Also dont put console log immediately after dispatching. As redux updates state, old state is not modified. Instead it creates a new state instance with updated value. This new value will be passed as props to your component via connect of react-redux.
Try printing display in render() method, you will see that it is called twice and second one will display true.
First, I would recommend not to rely on the fact that dispatching an action may be synchronous; design as if everything was asynchronous. When eventually you dispatch an async actions, you will be pleased to have your mindset ready for that.
Second, your action creator return a function (you must be using the thunk middleware), which is why you get this behaviour.
componentDidMount(){
startSomethingAsync();
}
componentDidUpdate(){
if (!this.props.asyncCompleted) return;
if(this.props.asyncResultFn) {
this.props.dispatch({ type: ... value: VALUE_CONDITIONAL_TRUE})
}
else{
this.props.dispatch({ type: ... value: VALUE_CONDITIONAL_FALSE})
}
}
I have a todo list that holds a delete button in a grandchild, that when clicked fires an event in the parent - I am wanting this event to delete the array entry corresponding to the grandchild clicked.
Parent (contains the array and my attempt at the function)
const tasks = [
{ name: 'task1', isComplete: false },
{ name: 'task2', isComplete: true },
{ name: 'task3', isComplete: false },
]
// taskToDelete is the name of the task - doesn't contain an object
deleteTask(taskToDelete) {
this.state.tasks.remove(task => task.name === taskToDelete);
this.setState({ tasks: this.state.tasks });
}
Any help would be appreciated
Two issues there:
You're seeming to try to direct modify this.state.tasks. It's important not to do that, never directly modify this.state or any object on it. See "Do Not Modify State Directly" in the React documentation for state.
You're passing an object to setState that is derived from the current state. It's important never to do that, too. :-) Instead, pass setState a function and use the state object it passes you when calling that function. From "State Updates May Be Asynchronous" in the documentation:
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state... [Instead]...use a second form of setState() that accepts a function rather than an object.
(my emphasis)
I figure your remove on an array was intended to be hypothetical, but for the avoidance of doubt, arrays don't have a remove method. In this case, the best thing to do, since we need a new array, is to use filter to remove all entries that shouldn't still be there.
So:
deleteTask(taskToDelete) {
this.setState(prevState => {
const tasks = prevState.tasks.filter(task => task.name !== taskToDelete);
return { tasks };
});
}
You could simply filter the array :
this.setState(prevState => ({
tasks: prevState.tasks.filter(task => task.name !== 'taskToDelete')
}));
Also when updating based on this.state, its better to use the function form because setState is async.
You can use filter to remove one object from an array following the immutable pattern (filter will create a new array) :
deleteTask(taskToDelete) {
const newTaskArray = this.state.tasks.filter(task => task.name !== taskToDelete);
this.setState({ tasks: newTaskArray });
}
Edit : codepend of the solution : https://codepen.io/Dyo/pen/ZvPoYP
You can implement deleteTask method as below:
deleteTask(taskToDelete) {
this.setState((prevState, props) => {
const tasks = [...prevState.tasks];
const indexOfTaskToDelete = tasks.findIndex(
task => task.name === taskToDelete
);
tasks.splice(indexOfTaskToDelete, 1);
return { tasks };
});
}
A. Find the index of taskToDelete.
B. Then use splice method to delete the item from the collection
C. Then call setState to update the state with tasks.
You can use higher order function Array#filter to delete the task.
let updatedTasks = this.state.tasks.filter(task => task.name !== taskToDelete);
this.setState({ tasks: updatedTasks });
I have followed below steps to delete a particular selected Object from the state array:
Here I am using a list of checkBoxes, when I am selecting a checkBox it will add it in the state array and when it gets de-selected then it will get deleted from the array.
if (checked) {
var tempObject = { checkboxValue: data, label: title }
this.state.checkBoxState.push(resTemp);
} else {
var element = data; //data is coming from different method.
for (let index = 0; index < this.state.checkBoxState.length; index++) {
if (element === this.state.checkBoxState[index].checkboxValue) {
this.state.checkBoxState.splice(index, 1);
}
}
}
I got stuck for this question and I am sharing my solution. Hope it will help you.
The first thing I tried was this:
const initialState = {
items: {},
showCart: false,
showCheckout: false,
userID: null
};
export default function reducer(state=Immutable.fromJS(initialState), action) {
case 'REMOVE_FROM_CART':
return state.deleteIn(['items', String(action.id)]);
}
When console logging the deleteIn above, it does actually remove the item from the Map correctly. However, the app doesn't re-render again, because I assume I'm mutating the state(?). (mapStateToProps gets called, but no new state).
So next I tried this:
case 'REMOVE_FROM_CART':
const removed = state.deleteIn(['items', String(action.id)]);
const removeItemState = {
...state,
items: { removed }
}
return state.mergeDeep(removeItemState);
But I'm just adding the deleted item to the items again, creating a duplication.
How can I handle this?
Have you tried removing the item after you've deeply cloned the state?
case 'REMOVE_FROM_CART':
const removeItemState = {
...state
items: {
...state.items
}
};
delete removeItemState.items[String(action.id)];
return removeItemState;
How about reduce?
case 'REMOVE_FROM_CART':
return {
...state,
items: Object.keys(state.items).reduce((acc, curr) => {
if (curr !== action.id) acc[curr] = state.items[curr];
return acc;
}, {})
};
Posting more code (such as my reducers setup) may have helped more, but here's what was going on:
First, this code was the right way to remove the item from the state.
return state.deleteIn(['items', String(action.id)]);
However, because I was using the immutable library and not redux-immutable for my combineReducers, my state was not properly being handled. This was allowing me to do things like state.cart.items (in mapStateToProps) where really I should've been using state.getIn(['cart', 'items']).
Changing that magically made the delete work.
Thanks to #jslatts in the Reactiflux Immutable Slack channel for help with figuring this out!
Is this a reasonable solution for data sharing between two states/reducers?
//combineReducers
function coreReducer(state = {}, action){
let filtersState = filters(state.filters, action);
let eventsState = events(state.events, action, { filters: filtersState});
return { events: eventsState, filters : filtersState};
}
export const rootReducer = combineReducers(
{
core : coreReducer,
users
}
);
If so, how can one guarantee the order in which reducer functions are executed if both answer to the same dispatched event and the second reducing function depends on the new state of the first one?
Let's say that we dispatch a SET_FILTER event that appends to activeFilters collection in the filters Store and later changes the visibility of items in the events Store with respect to the activeFilters values.
//ActiveFilters reducer
function filtersActions(state = {}, action){
switch (action.type) {
case SET_FILTER:
return Object.assign({}, state, {
[action.filterType]: action.filter
})
case REMOVE_FILTER:
var temp = Object.assign({}, state);
delete temp[action.filterType];
return temp;
case REMOVE_ALL_FILTERS:
return {};
default:
return state
}
}
I think I found the answer - Computing Derived Data - Reselect
http://redux.js.org/docs/recipes/ComputingDerivedData.html
/--------container--------/
import {getGroupsAndMembers} from '../reducers'
const mapStateToProps = (state) => {
return {
inputValue: state.router.location.pathname.substring(1),
initialState: getGroupsAndMembers(state) <-- this one
}
}
/--------reducers--------/
export function getGroupsAndMembers(state){
let { groups, members } = JSON.parse(state)
response = {groups, members}
return response;
}
GroupsContainer.propTypes = {
//React Redux injection
pushState: PropTypes.func.isRequired,
// Injected by React Router
children: PropTypes.node,
initialState:PropTypes.object,
}
don't forget to follow the guidelines for 'connect'
export default connect(mapStateToProps,{ pushState })(GroupsContainer)
If you have two reducers, and one depend on a value from a first one, you just have to update them carefully, and the best solution will be just to use a special function, which will first set the filtering, and then query corresponding events. Also, keep in mind that if events fetching is asynchronous operation, you should also nest based on filtering type -- otherwise there is a chance of race condition, and you will have wrong events.
I have created a library redux-tiles to deal with verbosity of raw redux, so I will use it in this example:
import { createSyncTile, createTile } from 'redux-tiles';
const filtering = createSyncTile({
type: ['ui', 'filtering'],
fn: ({ params }) => params.type,
});
const events = createTile({
type: ['api', 'events'],
fn: ({ api, params }) => api.get('/events', { type: params.type }),
nesting: ({ type }) => [type],
});
// this function will just fetch events, but we will connect to apiEvents
// and filter by type
const fetchEvents = createTile({
type: ['api', 'fetchEvents'],
fn: ({ selectors, getState, dispatch, actions }) => {
const type = selectors.ui.filtering(getState());
return dispatch(actions.api.events({ type }));
},
});