I'm trying to find a way to pass a state to an action or a reducer. For example
I want to be able to run the onDelete function on the action then update the state on the reducer. However, in order for this to work, i would need to filter through the posts then i would able to remove a post.
class Posts extends Component {
state = {
posts: [],
loading: true,
}
getPosts = () => {
Axios.get(process.env.REACT_APP_GET_POSTS)
.then( (res) => {
this.setState({
posts: res.data,
loading: false
})
})
// console.log(this.state.posts);
}
componentWillMount(){
this.getPosts();
}
// run this logic on the reducer or on actions.
onDelete = (id) => {
Axios.post(`/api/posts/delete/${id}`);
this.setState({
posts: this.state.posts.filter(post => post.id !== id)
})
}
render() {
const {loading, posts} = this.state;
if (!this.props.isAuthenticated) {
return (<Redirect to='/signIn' />);
}
if(loading){
return "loading..."
}
return (
<div className="App" style={Styles.wrapper}>
<h1> Posts </h1>
<PostList DeletePost={this.onDelete} posts={posts}/>
</div>
);
}
}
Here is the attempt to make into an action, which technically works.
actions
export const DeletePost = (id) => {
return (dispatch) => {
return Axios.post(`/api/posts/delete/${id}`)
.then( () => {
dispatch({type: DELETE_POST, id});
});
}
}
Then we approach the problem of actually getting the posts on the reducer. The problem is that the reducer does not know where the posts are coming from, its undefined. So i want to know how would i pass the state to the reducer.
and will return
state.posts.filter is not a function or something along those lines.
reducer.js
import { DELETE_POST} from '../actions/';
const initialState = {
post: [],
postError: null,
posts:[]
}
export default (state = initialState, action) => {
switch (action.type) {
case DELETE_POST:
return ({
...state,
posts: state.posts.filter(post=> post.id !== action.id)
})
default:
return state
}
}
How would i get pass the state to the actions, so that i would be able to update the state on the reducer ?
I'm trying to find a way to pass a state to an action or a reduce
The way you wrote your actions code indicates you're using redux thunk, which means you can access the getState function in your action. Example usage of getState is here
export const DeletePost = (id) => {
return (dispatch, getState) => {
return Axios.post(`/api/posts/delete/${id}`)
.then( () => {
dispatch({type: DELETE_POST, id});
});
}
}
you already have access to the state in your reducer code. Its called state!
Now, the above could the end of the answer. But I'm questioning the premise of what you're doing in the class.
// run this logic on the reducer or on actions.
onDelete = (id) => {
Axios.post(`/api/posts/delete/${id}`);
this.setState({
posts: this.state.posts.filter(post => post.id !== id)
})
}
Above you're filtering for the posts after you've already filtered/deleted it from redux (i.e. you're filtering unnecessarily twice). You should instead just be getting the state directly from redux
Take a look here. For an example of this being used in a more robust setting. I would direct you to this example. For the example, look at src/containers/visibleTodoList
So really for what you're doing, posts should just live with redux and not in the class component!
Lastly for the error you saw
state.posts.filter is not a function or something along those lines.
Could you give the exact error? your reducer code seems fine.
Related
I'm creating a new app where I want to be able to post updates to my friends. A micro-blogging site.
I want to learn how to update the app using React hooks and React's context API. I created the following provider that takes the state as the value... I want to be able to add a new post and then update the state's posts so that I don't have to fetch the database again (using firestore) I'm really trying to save myself a call to the db...
Basically, when I call createNewPost within the state, I want to be able to update the current posts section of the state: state.posts but when I update the state after the API call is successful, my entire posts array gets replaced for some reason. Not sure what I might be doing wrong...
import { createContext, useState } from 'react';
import { createDoc, getWhere } from '../utils/database/db';
export const PostDataContext = createContext();
const SetDataContextProvider = ({ children }) => {
const [state, setState] = useState({
posts: [],
timelinePosts: [],
createNewPost: async (collection, payload) => {
const doc = await createDoc(collection, payload)
payload.id = doc?.doc?.id;
updateStatePosts(payload);
return doc;
},
getPostsByUserId: async (userId) => {
const dataReceived = await getWhere('/posts', userId, 'userId')
setState({ ...state, posts: dataReceived })
}
});
const updateStatePosts = (payload) => {
console.log('why is state posts empty?!', state);
setState({ ...state, posts: [payload, ...state.posts] })
}
return <PostDataContext.Provider value={state}>
{children}
</PostDataContext.Provider>
}
export default SetDataContextProvider;
If I had to guess I would say you have a stale enclosure of your initial empty posts state within the updateStatePosts function used in your state. You can use a functional state update to access the previous state to update from. Functional state updates allow you to update from the previous state, not the state the update was enqueued/enclosed in.
const SetDataContextProvider = ({ children }) => {
const [state, setState] = useState({
posts: [],
timelinePosts: [],
createNewPost: async (collection, payload) => {
const doc = await createDoc(collection, payload)
payload.id = doc?.doc?.id;
updateStatePosts(payload);
return doc;
},
getPostsByUserId: async (userId) => {
const dataReceived = await getWhere('/posts', userId, 'userId')
setState(prevState => ({
...prevState, // <-- preserve any previous state
posts: dataReceived
}))
}
});
const updateStatePosts = (payload) => {
setState(prevState => ({ // <-- previous state to this update
...prevState,
posts: [payload, ...prevState.posts],
}));
};
return <PostDataContext.Provider value={state}>
{children}
</PostDataContext.Provider>
}
This function is wrong:
const updateStatePosts = (payload) => {
console.log('why is state posts empty?!', state);
setState({ ...state, posts: [payload, ...state.posts] })
}
You're using the spread operator correctly for your state, but posts is directly replaced. You might want to use the following setState:
setState({ ...state, posts: [...payload,...state.posts] })
Despite that, you should also refactor your state. Functions are not states so put them outside your state.
I'm learning React and Redux. And I may have a really basic question.
I want to get a single story from my backend using the Redux function mapStateToProps (#1). So I wrote the function getSingleStory which takes the id as argument and returns the story data (#2). When I log the response data of the getSingleStory in the console, it shows me the correct story fetched from the backend (#3):
However, if the console logs the story array in my component (#4), it outputs all stories from my database, not just the single story I wanted to fetch (see picture). If I want to display 'Story.title', in my render function of course it does not work.
If someone could explain to me why in the response data the single story is included and in the const story = this.props.story; all stories suddenly appear, that would help me a lot.
export class StoryDetails extends Component {
componentDidMount() { // #2
this.props.getSingleStory(this.props.match.params.id);
}
render() {
const story = this.props.story;
console.log (story); // #4
return (
<div>
<h2>{story.title}</h2>
</div>
);
}
}
const mapStateToProps = state => ({story: state.story}); //#1
export default connect(
mapStateToProps,
{ getSingleStory, deleteStory}
)(StoryDetails);
Action
// GET SINGLE STORY
export const getSingleStory = id => (dispatch, getState) => {
return new Promise((resolve, reject) => {
axios.get( apiBase + `/story/${id}/`, tokenConfig(getState))
.then(res => {
dispatch({
type: GET_SINGLE_STORY,
story: res.data
}, console.log (res.data)); //#3
resolve(res);
})
.catch(err => {
dispatch(returnErrors(err.response.data, err.response.status));
reject(err);
});
});
};
Reducer
import { GET_SINGLE_STORY } from "../actions/types.js";
export default function (state = {}, action) {
switch (action.type) {
case GET_SINGLE_STORY:
return action.story;
default:
return state;
}
};
Many Thanks in advance!
So i'm doing a API GET request and set the data on reducer, but the component render twice, first before dispatch and another after, the first one is causing map function problem
what can i do to avoid render twice and solve map function problem?
App.js
componentDidMount(){
this.props.carregarLojas();
}
render(){
const { lojasTeste } = this.props;
//rendering 2 times
console.log(lojasTeste);
return(
<div>
lojasTeste.map((i, index) => (
<h1>{i.name}</h1>
))
</div>
)
}
const mapStateToProps = store => ({
lojasTeste: store.lojaState.lojasTeste
});
const mapDispatchToProps = dispatch => {
return {
carregarLojas: () => {
dispatch(carregarLojas());
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
Action.js
export const setarLojas = (lojas) =>{
return {
type: SETAR_LOJAS,
data: lojas
}
}
export const carregarLojas = () => {
return (dispatch) => {
return API.get('loja')
.then(response => {
dispatch(setarLojas(response.data))
})
.catch(error => {
throw(error);
})
}
Reducer.js
const initialState ={
lojasTeste: {}
}
export const lojaReducer = (state = initialState, action) => {
switch (action.type){
case SETAR_LOJAS:
return {
...state,
lojasTeste: action.data
}
default:
return state;
}
}
The double render is totally normal:
Your component render once, then call the carregarLojas method which is async. When resolved, the method will update your redux store, which is connected with the props of your component (mapStateToProps). When a prop is updated, it cause automatically a rerender.
Also, for your map problem, you didn't initialized lojasTeste as an array, but as an object. You can't use map on an object (cf https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Array/map)
Summary: I am trying to pass a function down to a presentational component that will dispatch an action. The first argument (a key) must be defined in the container. A second argument (a page number) would be supplied in the presentational component. I'm suspecting I am doing something dumb.
I'm new to this so forgive me if my terminology is off or the example is poor. Here is what's going on:
const mapDispatchToProps = (dispatch) => ({
changePage: bindActionCreators(changePosts, dispatch)
})
The purpose of this function is to change the page number of a certain group of posts in the store.
export const changePage = key => (dispatch, getState) => page => {
return dispatch(onChangePage(key, page)); //changes the page of the key obj
}
What I was hoping to do was pass this down to a presentational component like so:
<Posts changePage={this.props.changePosts('news') />
Now inside the Posts component all I would have to do is
<a onClick={this.props.changePage(4)}>4</a>
to go to page 4. Essentially I'm telling the presentational component to change to page 4, but only where the key is 'news', which I defined a level up in the container.
This isn't working. The reason it isn't is because a 4 isn't being passed, but instead a Proxy Object:
[[Handler]]:Object
[[Target]]:SyntheticMouseEvent
[[IsRevoked]]:false
Am I misunderstanding how currying/dispatch works? I really appreciate any help.
Edit: more code
// container
class Index extends Component {
static async getInitialProps({ store, isServer }) {
await store.dispatch(fetchPosts('news'));
return { ...store.getState() };
}
constructor(props) {
super(props);
}
render() {
const { nextPage, previousPage, changePage } = this.props;
const { posts } = this.props.wordpress;
const { news } = posts;
return (
<Main>
<Posts
next={()=>nextPage('news')}
previous={()=>previousPage('news')}
changePosts={()=>changePage('news')}
items={news.items}
/>
</Main>
);
}
}
const mapStateToProps = ({ wordpress }) => ({
wordpress
})
const mapDispatchToProps = (dispatch) => ({
fetchPosts: bindActionCreators(fetchPosts, dispatch),
nextPage: bindActionCreators(nextPosts, dispatch),
previousPage: bindActionCreators(previousPosts, dispatch),
changePage: bindActionCreators(changePosts, dispatch)
})
export default withRedux(initStore, mapStateToProps, mapDispatchToProps)(Index);
// actions
export const changePage = key => (dispatch, getState) => page => {
console.log(page) // this returns a proxy object for some reason
return dispatch(changePage(key, page));
}
// The only real thing in the presentational component
<a onClick={props.changePosts(4)}>4</a>
Previous and Next buttons work
1) Firstly I should mention that mapDispatchToProps in your example looks a bit weird, because you only need to use bindActionCreators in case if you supply it with an object of actions. Most likely a simple object form of mapDispatchToProps would be enough in your case:
const mapDispatchToProps = {
fetchPosts,
nextPage,
previousPage,
changePage,
};
2) Secondly the following piece of code is really confusing, why is it dispatching itself recursively?
// actions
export const changePage = key => (dispatch, getState) => page => {
console.log(page) // this returns a proxy object for some reason
return dispatch(changePage(key, page));
}
3) Finally, provided you already have an action creator changePage somewhere in you code that accepts args key and page, and it was added to mapDispatchToProps as I've mentioned earlier:
// in your container
<Posts
...
changePosts={page => props.changePage('news', page)}
...
/>
// in your component
<a onClick={() => props.onClick(4)}>4</a>
I do not know how to access a boolean isLoading flag from reducerForm.js reducer in reducerRegister.js. I have used combineReducers() and I use isLoading to disable a button during form submit.
It's initial state is false, after clicking submit, it changes to true. After the form submission is successful, isLoading is reset to false again. Below is the relevant code for this issue:
actionRegister.js
let _registerUserFailure = (payload) => {
return {
type: types.SAVE_USER_FAILURE,
payload
};
};
let _registerUserSuccess = (payload) => {
return {
type: types.SAVE_USER_SUCCESS,
payload,
is_Active: 0,
isLoading:true
};
};
let _hideNotification = (payload) => {
return {
type: types.HIDE_NOTIFICATION,
payload: ''
};
};
// asynchronous helpers
export function registerUser({ // use redux-thunk for asynchronous dispatch
timezone,
password,
passwordConfirmation,
email,
name
}) {
return dispatch => {
axios.all([axios.post('/auth/signup', {
timezone,
password,
passwordConfirmation,
email,
name,
is_Active: 0
})
// axios.post('/send', {email})
])
.then(axios.spread(res => {
dispatch(_registerUserSuccess(res.data.message));
dispatch(formReset());
setTimeout(() => {
dispatch(_hideNotification(res.data.message));
}, 10000);
}))
.catch(res => {
// BE validation and passport error message
dispatch(_registerUserFailure(res.data.message));
setTimeout(() => {
dispatch(_hideNotification(res.data.message));
}, 10000);
});
};
}
actionForm.js
export function formUpdate(name, value) {
return {
type: types.FORM_UPDATE_VALUE,
name, //shorthand from name:name introduced in ES2016
value
};
}
export function formReset() {
return {
type: types.FORM_RESET
};
}
reducerRegister.js
const INITIAL_STATE = {
error:{},
is_Active:false,
isLoading:false
};
const reducerSignup = (state = INITIAL_STATE , action) => {
switch(action.type) {
case types.SAVE_USER_SUCCESS:
return { ...state, is_Active:false, isLoading: true, error: { register: action.payload }};
case types.SAVE_USER_FAILURE:
return { ...state, error: { register: action.payload }};
case types.HIDE_NOTIFICATION:
return { ...state , error:{} };
}
return state;
};
export default reducerSignup;
reducerForm.js
const INITIAL_STATE = {
values: {}
};
const reducerUpdate = (state = INITIAL_STATE, action) => {
switch (action.type) {
case types.FORM_UPDATE_VALUE:
return Object.assign({}, state, {
values: Object.assign({}, state.values, {
[action.name]: action.value,
})
});
case types.FORM_RESET:
return INITIAL_STATE;
// here I need isLoading value from reducerRegister.js
}
return state;
};
export default reducerUpdate;
reducerCombined.js
import { combineReducers } from 'redux';
import reducerRegister from './reducerRegister';
import reducerLogin from './reducerLogin';
import reducerForm from './reducerForm';
const rootReducer = combineReducers({
signup:reducerRegister,
signin: reducerLogin,
form: reducerForm
});
export default rootReducer;
This is where I use isLoading:
let isLoading = this.props.isLoading;
<FormGroup>
<Col smOffset={4} sm={8}>
<Button type="submit" disabled={isLoading}
onClick={!isLoading ? isLoading : null}
>
{ isLoading ? 'Creating...' : 'Create New Account'}
</Button>
</Col>
</FormGroup>
Mapping state to props within the same component
function mapStateToProps(state) {
return {
errorMessage: state.signup.error,
isLoading: state.signup.isLoading,
values: state.form.values
};
}
This is covered in the Redux FAQ at https://redux.js.org/faq/reducers#how-do-i-share-state-between-two-reducers-do-i-have-to-use-combinereducers:
Many users later want to try to share data between two reducers, but find that combineReducers does not allow them to do so. There are several approaches that can be used:
If a reducer needs to know data from another slice of state, the state tree shape may need to be reorganized so that a single reducer is handling more of the data.
You may need to write some custom functions for handling some of these actions. This may require replacing combineReducers with your own top-level reducer function. You can also use a utility such as reduce-reducers to run combineReducers to handle most actions, but also run a more specialized reducer for specific actions that cross state slices.
Async action creators such as redux-thunk have access to the entire state through getState(). An action creator can retrieve additional data from the state and put it in an action, so that each reducer has enough information to update its own state slice.
A reducer cannot access another reducer's state, but if you're using redux-thunk you can do so from within an action creator. As an example, you can define an action creator like this:
export const someAction = () =>
(dispatch, getState) => {
const someVal = getState().someReducer.someVal;
dispatch({ type: types.SOME_ACTION, valFromOtherReducer: someVal });
};
React Redux works on unidirectional data flow.
Action ---> Reducer /store ---> Reducer
Reducer works on small subset of store, you can not access store inside reducer which is not part of Reducer. you can either need to fire new action from the component based on reducer state return.