How to curry a dispatch function with react/redux? - javascript

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>

Related

React and Redux: Onclick Delete Button Returning Undefined

In my code below, I have a delete button that should be deleting the data if clicked. However, when I click on it, I am seeing through console.log that it is returning undefined instead of the id number. Can't seem to figure out why. Any help will be greatly appreciated. Thank you.
//Actions File
export const GET_ITEMS = 'GET ITEMS';
export const FETCH_ITEMS_SUCCESS = 'FETCH ITEMS SUCCESS';
export const FETCH_ITEMS_ERROR = 'FETCH ITEMS ERROR';
export const DELETE_ITEM = 'DELETE_ITEM';
export const getItems = () => ({
type: GET_ITEMS
});
export const deleteItem = (itemId) => ({
type : DELETE_ITEM,
payload: itemId
});
//App.js
class App extends Component {
componentDidMount() {
this.props.getItems()
}
static propTypes = {
getItems: PropTypes.func.isRequired,
deleteItem: PropTypes.func.isRequired
}
handleDelete = (id) =>{
this.props.deleteItem(id)
console.log(this.props.deleteItem(id));
}
render() {
const { itemsList} = this.props.items
return (
<div className="container app-wrapper">
<header>
{itemsList.map(item => (<h1 key={item.id}>{item.title} <button onClick={this.handleDelete.bind(this, item.id)}>delete</button></h1>))}
</header>
</div>
);
}
}
const mapStateToProps = state => ({
items: state.items
});
export default connect(mapStateToProps, {getItems, deleteItem})(App);
The dispatched action should return undefined, because it does not return anything. You are misunderstanding how data flows in the Redux/reducer pattern.
Here's the basic flow of a Redux update:
Action is dispatched.
All reducers receive the action object.
All reducers return their new or previous state depending on that action's contents.
connect sees that the Redux state has changed, and triggers a re-render of the children components.
You may now use the updated data from your Redux store through props (mapped in mapStateToProps).
You cannot call an action and receive the updated state as the return value. It breaks the fundamental pattern of how data flows/updates in Redux.
You are referencing your delete action incorrectly in connect. deleteItem expects an id param passed into it.
Try this,
const mapStateToProps = state => ({
items: state.items
});
const mapDispatchToProps = (dispatch) =>
{
return {
deleteItem: (id) => dispatch(actions.deleteItem(id)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);

How to dispatch a new message in a dynamically loaded button?

I'm trying to dispatch a message whenever I change a room - which load dynamically,
I have a component that looks like that:
const Rooms = (props) => {
return(
<div className='rooms-styles'>
{rooms.length === 0 && <p>This server is missing rooms!! WERE GOING TO DIEEE!!</p>}
<div>
{rooms.length > 0 && rooms.map((roomBtn)=><button onClick={()=>{/* Need to dispatch here! */}}>{roomBtn.roomName}</button>)}
</div>
</div>
);
}
and I'm trying to change a state property (that's how it's called?) called 'room'.
What I've tried so far:
set a mapStateToProps and mapDispatchToProps like that:
selectedRoom is the selected room, being set at the button click as selectedRoom = roomBtn.roomName
const mapStateToProps = (state) => {
return {
room: selectedRoom
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({changeRoom}, dispatch);
};
export default connect(mapStateToProps ,mapDispatchToProps)(Rooms);
use props.dispatch(changeRoom(roomBtn.roomName)) when changeRoom is an action (didn't work because I don't have access to props inside onClick)
github page: https://github.com/Ido-Levi/Playground
You are incorrectly using the mapDispatchToProps function
https://react-redux.js.org/using-react-redux/connect-mapdispatch
You must return an object which will have the key that is the prop to the component
const mapDispatchToProps = (dispatch) => {
return {changeRoomDispatch : bindActionCreators({changeRoom}, dispatch)};
};
and then use onClick={() => props.changeRoomDispatch(roomBtn.roomName)}
Alternatively
you can use connect without the second argument for mapDispatchToProps as
export default connect(mapStateToProps)(Rooms);
and have access to the dispatch prop inside the function and then onClick={() => props.dispatch(changeRoom(roomBtn.roomName))} should work fine.
refer the link above for documentation

How to pass state as a redux action

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.

Re-rendering react component after clicking Like button (with Redux)

I have the following React component that shows all the users posts through the "renderPosts" method. Below it there's a like/unlike button on whether the currently logged in user has liked the post.
However, when I click on the like button, the component does not re-render in order for the "renderPosts" method to create an unlike button and the "like string" is modified as expected. Only when I go to another component and then come back to this component does the unlike button display and vice versa.
Is there anyway that I could fix this with Redux in my app? I tried this.forceUpdate after the onClick event but still does not work...
Also I tried creating a new Reducer called "likers", according to robinsax which basically get the array of users who like a particular post and imported it as props into the component but got
"this.props.likers.includes(currentUser)" is not a function
When the app first gets to the main page (PostIndex), probably because this.props.likers is still an empty object returned from reducer
Here is the code for my action creator:
export function likePost(username,postId) {
// body...
const request = {
username,
postId
}
const post = axios.post(`${ROOT_URL}/likePost`,request);
return{
type: LIKE_POST,
payload: post
}
}
export function unlikePost(username,postId){
const request = {
username,
postId
}
const post = axios.post(`${ROOT_URL}/unlikePost`,request);
return{
type: UNLIKE_POST,
payload: post
}
}
And this is my reducer:
import {LIKE_POST,UNLIKE_POST} from '../actions/index.js';
export default function(state = {},action){
switch(action.type){
case LIKE_POST:
const likers = action.payload.data.likedBy;
console.log(likers);
return likers;
case UNLIKE_POST:
const unlikers = action.payload.data.likedBy;
console.log(unlikers);
return unlikers;
default:
return state;
}
}
I would really appreciate any help since I'm a beginner
import { fetchPosts } from "../actions/";
import { likePost } from "../actions/";
import { unlikePost } from "../actions/";
class PostsIndex extends Component {
componentDidMount() {
this.props.fetchPosts();
}
renderPost() {
const currentUser = Object.values(this.props.users)[0].username;
return _.map(this.props.posts, post => {
return (
<li className="list-group-item">
<Link to={`/user/${post.username}`}>
Poster: {post.username}
</Link>
<br />
Created At: {post.createdAt}, near {post.location}
<br />
<Link to={`/posts/${post._id}`}>{post.title}</Link>
<br />
//error here, with this.props.likers being an
//array
{!this.props.likers.includes(currentUser) ? (
<Button
onClick={() => this.props.likePost(currentUser,post._id)}
bsStyle="success"
>
Like
</Button>
) : (
<Button
onClick={() => this.props.unlikePost(currentUser,post._id)}
bsStyle="warning"
>
Unlike
</Button>
)}{" "}
{post.likedBy.length === 1
? `${post.likedBy[0]} likes this`
: `${post.likedBy.length} people like this`}
</li>
);
});
}
function mapStateToProps(state) {
return {
posts: state.posts,
users: state.users,
likers: state.likers
};
}
}
Seems like the like/unlike post functionality isn't causing anything in your state or props to change, so the component doesn't re-render.
You should change the data structure you're storing so that the value of post.likedBy.includes(currentUser) is included in one of those, or forceUpdate() the component after the likePost and unlikePost calls.
Please do it the first way so I can sleep at night. Having a component's render() be affected by things not in its props or state defeats the purpose of using React.
As noted in other answers, you need to use redux-thunk or redux-saga to make async calls that update you reducer. I personally prefer redux-saga. Here's is a basic implementation of React, Redux, and Redux-Saga.
Redux-Saga uses JavaScript generator functions and yield to accomplish the goal of handling async calls.
Below you'll see a lot of familiar React-Redux code, the key parts of Redux-Saga are as follows:
watchRequest - A generator function that maps dispatch actions to generator functions
loadTodo - A generator function called from watchRequest to yield a value from an async call and dispatch an action for the reducer
getTodoAPI - A regular function that makes a fetch request
applyMiddleware - from Redux is used to connect Redux-Saga with createStore
const { applyMiddleware, createStore } = Redux;
const createSagaMiddleware = ReduxSaga.default;
const { put, call } = ReduxSaga.effects;
const { takeLatest } = ReduxSaga;
const { connect, Provider } = ReactRedux;
// API Call
const getTodoAPI = () => {
return fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => {
return response.json()
.then(response => response);
})
.catch(error => {
throw error;
})
};
// Reducer
const userReducer = (state = {}, action) => {
switch (action.type) {
case 'LOAD_TODO_SUCCESS':
return action.todo;
default:
return state;
}
};
// Sagas, which are generator functions
// Note: the asterix
function* loadTodo() {
try {
const todo = yield call(getTodoAPI);
yield put({type: 'LOAD_TODO_SUCCESS', todo});
} catch (error) {
throw error;
}
}
// Redux-Saga uses generator functions,
// which are basically watchers to wait for an action
function* watchRequest() {
yield* takeLatest('LOAD_TODO_REQUEST', loadTodo);
}
class App extends React.Component {
render() {
const { data } = this.props;
return (
<div>
<button onClick={() => this.props.getTodo()}>Load Data</button>
{data ?
<p>data: {JSON.stringify(data)}</p>
: null
}
</div>
)
}
}
// Setup React-Redux and Connect Redux-Saga
const sagaMiddleware = createSagaMiddleware();
const store = createStore(userReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(watchRequest);
// Your regular React-Redux stuff
const mapStateToProps = (state) => ({ data: state }); // Map the store's state to component's props
const mapDispatchToProps = (dispatch) => ({ getTodo: () => dispatch({type: 'LOAD_TODO_REQUEST'}) }) // wrap action creator with dispatch method
const RootComponent = connect(mapStateToProps, mapDispatchToProps)(App);
ReactDOM.render(
<Provider store={store}>
<RootComponent />
</Provider>,
document.getElementById('root')
);
<script src="https://npmcdn.com/babel-regenerator-runtime#6.3.13/runtime.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/6.0.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-saga/0.16.2/redux-saga.min.js"></script>
<div id="root"></div>
You need to use redux-thunk middleware in order to use async actions.
First, add redux-thunk while creating store like
import thunk from 'redux-thunk';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
then change your method like this
export function likePost(username,postId) {
return function(dispatch) {
// body...
const request = {
username,
postId
}
axios.post(`${ROOT_URL}/likePost`,request)
.then(res => {
dispatch({
type: LIKE_POST,
payload: res
});
});
}
}
and now in your component after mapStateToProps, define mapDispatchToProps,
const mapDispatchToProps = dispatch => {
return {
likePost: (currentUser,postId) => dispatch(likePost(currentUser, postId)),
// same goes for "unlike" function
}
}
export default connect(mapStateToProps, mapDispatchToProps)(PostsIndex);
The problem is in your action creator.
export function likePost(username,postId) {
// body...
const request = {
username,
postId
}
// this is an async call
const post = axios.post(`${ROOT_URL}/likePost`,request);
// next line will execute before the above async call is returned
return{
type: LIKE_POST,
payload: post
}
}
Because of that your state is likely never updated and stays in the initial value.
You would need to use either redux-thunk or redux-saga to work with async actions.
As they say use redux-thunk or redux-saga. If your new to redux I prefer redux-thunk because it's easy to learn than redux-saga. You can rewrite your code like this
export function likePost(username,postId) {
// body...
const request = {
username,
postId
}
const post = axios.post(`${ROOT_URL}/likePost`,request);
return dispatch => {
post.then(res => {
dispatch(anotherAction) //it can be the action to update state
});
}
}

Redux Actions must be plain objects. Use custom middleware for async actions

The code is:
Test component:
import {add} from './../actions/';
class Test extends Component{
_add = (){
this.props.add(1);
}
render(){
<button onClick={this._add}>add</button>
}
}
const mapStateToProps = ()=>({
haha:'haha'
});
export default connect(
mapStateToProps,
{add}
)(Test)
Actions:
export const add = value => (dispatch) =>{
dispatch({
type:'ADD',
value:value
})
}
I click add button there has this error!
What's the issue?
I looked createStore.js and console.log(action). It shows a function.
But Redux's example is not a function. My code is almost the same.
If you use redux-thunk as a middleware, it will process dispatched function actions....
Another one is redux-promise which will do somethink like the thunk ... but with promises
UPDATE:
This is a model to handle async
export const add = value => (dispatch) => {
... do something async
}
LIke this:
export const add = value => (dispatch) => {
http.get('/path')
.then(json =>
dispatch({type: 'RESULT', payload: json}));
}
You action does not have async calls so it could be written like this:
export const add = value => ({
type:'ADD',
value:value
})
You are simply missing the arrows => in your arrow function:
export const add = value => (dispatch) => {
dispatch({
type:'ADD',
value:value
})
}
You should write your action creator like this:
export const add = value =>
({
type: 'ADD',
value: value
});
The way you connect your action creator to your component (passing it with the shortcut notation { add } as a second parameter to connect) allows you to omit the dispatch since connect will automatically wrap your action creator into a dispatch call when called this way.

Categories