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

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
});
}
}

Related

React-Redux: Action Dispatch on button click not working

On click of Button my Action is not getting dispatched. Below are all the files viz - action, reducer, root reducer, configSTore, Index and Component.
Please help me why my action is not getting dispatched on button click
Actions.js
import axios from 'axios';
const requestURL='JSONUrl';
let responseData = '';
export function fetchRequests() {
axios.get(requestURL)
.then((res) => {
responseData = res;
});
return responseData;
}
export const fetchDataAction = () => {
return {
type: 'FETCH_DATA',
data: fetchRequests()
};
}
export function fetchDataSuccessAction(err) {
return {
type: 'FETCH_DATA_SUCCESS',
err
};
}
export function fetchDataErrorAction(err) {
return {
type: 'FETCH_DATA_ERROR',
err
};
}
Reducer.js
export function fetchDataReducer(state = [], action) {
switch(action.type) {
case 'FETCH_DATA':
return action.data;
default: return state;
}
}
export function fetchDataSuccessReducer(state = [], action) {
switch(action.type) {
case 'FETCH_DATA_SUCCESS':
return action.err;
default: return state;
}
}
export function fetchDataErrorReducer(state = [], action) {
switch(action.type) {
case 'FETCH_DATA_ERROR':
return action.err;
default: return state;
}
}
RootReducer
import {combineReducers} from 'redux';
import { fetchDataAction, fetchDataSuccessAction, fetchDataErrorAction}
from '../actions/fetchData';
export default combineReducers({
fetchDataAction,
fetchDataSuccessAction,
fetchDataErrorAction
});
configStore.js
import { createStore, applyMiddleware } from "redux";
import rootReducer from "../reducers/rootReducer";
import thunk from 'redux-thunk';
export default function configureStore() {
const enhance = applyMiddleware(thunk);
return createStore(
rootReducer,
enhance
);
}
INdex.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import reportWebVitals from './reportWebVitals';
import Header from './App';
import configureStore from './store/configureSTore';
import {CounterApp} from './counter';
import { Provider } from 'react-redux';
ReactDOM.render(
<Provider store={configureStore()}>
<Header favcol="yellow"/>
<CounterApp />
</Provider>,
document.getElementById('root')
);
reportWebVitals();
My Component File
import React, { useState } from 'react';
import { fetchDataAction, fetchRequests } from './actions/fetchData';
import { connect } from 'react-redux';
export class CounterApp extends React.Component {
constructor(props) {
super(props);
}
btnClick = () => {
return this.props.fetchDataAction;
}
render() {
return (
<div className="App">
<h1>Hello</h1>
<div>
<h1>Fetch Data click below</h1>
<button onClick={() => this.props.fetchDataAction}>
Fetch Data
</button>
{this.props.datafetchedFromApi}
</div>
</div>
)
}
}
const mapStateToProps = state => ({
datafetchedFromApi: state.data
});
const mapDispatchToProps = (dispatch) => ({
fetchDataAction: () => dispatch(fetchDataAction())
});
export default connect(mapStateToProps, mapDispatchToProps)(CounterApp);
On click of Button my Action is not getting dispatched. Below are all the files viz - action, reducer, root reducer, configSTore, Index and Component.
Please help me why my action is not getting dispatched on button click
The answer got too long, so first part is if you wanna fully understand why things went south. If you just want your problem solved so you can finally go pee, the second part is for you :).
First Part (Basically what asynchronous programming in JavaScript is, so any questions to what are asynchronous tasks in JS can be referred to this answer.)
Okay a couple of problems detected here. As others have pointed out make sure all the paths for imports are correct. Now assuming they are all correct, here's what you need to do to solve your problem.
First let's take a look at the top of your component file:
import React, { useState } from 'react';
import { fetchDataAction, fetchRequests } from './actions/fetchData';
import { connect } from 'react-redux';
...
And then the block where you have called fetchDataAction:
...
<button onClick={() => this.props.fetchDataAction}>
Fetch Data
</button>
...
Here what you have done is this.props.fetchDataAction, I don't see you passing fetchDataAction as a prop to this component, so it's most probably undefined that's why you get an error TypeError: this.props.fetchDataAction is not a function because of course undefined is not a function.
This was the first mistake I noticed.
Now, moving on to the second one. I'm gonna start with this
Dispatching actions in redux is synchronous.
So you cannot do something like the following:
export default function SomeComponent(props) {
const fetchAction = () => {
let payload;
//wait for 5 seconds for the async task(the setTimeout below is an async task) to populate the payload with data.
setTimeout(() => payload = "This is my data", 5000);
//then return the action object
return {
type: 'Data',
payload,
};
}
const handleClick = () => {
dispatch(fetchAction());
}
return (<>
<Button onClick={handleClick} />
</>);
}
The above will not throw any errors, but will certainly not do what I want it to do. The above will dispatch the following action object:
{
type: 'Data',
payload: undefined
}
which ofcourse is not what I want the payload to be.
And that's exactly what you're doing. Take a look at your fetchDataAction and fetchRequests functions:
...
let responseData = '';
export function fetchRequests() {
axios.get(requestURL)
.then((res) => {
responseData = res;
});
return responseData;
}
export const fetchDataAction = () => {
return {
type: 'FETCH_DATA',
data: fetchRequests()
};
}
...
Now I'll compare with the example I've given above:
Here your responseData is analogous to my payload
Your fetchRequests function is analogous to my setTimeout
Looks familiar? I'm sure by now it does. Plain simple answer as to why it doesn't work is that you're performing an async task, in your case you're making a network request with axios.get(requestUrl)...
Network requests are async(now if you don't know what async things are, check out https://javascript.info/callbacks which gives you idea about what those are. Also check out a video on it by TheNetNinja https://www.youtube.com/watch?v=ZcQyJ-gxke0 ), in simple words, \network requests take some time to get finished(just like setTimeout).
So the axios.get request takes some time to get the response back from the server. Now the other tasks(below it) won't wait for that request to get completed, instead js will execute those tasks immediately without waiting for the response.
I know this answer is getting too long. But I want you to understand, because trust me I have made the same mistakes before :).
So in your fetchRequests function:
export function fetchRequests() {
axios.get(requestURL) --- (1)
.then((res) => {
responseData = res; --- (2)
});
return responseData; --- (3)
}
In line 1, you start an async task. Remember the function inside then block will execute only after sometime. So the responseData is still undefined. Instead of line (2), line (3) will execute first, cause as I told you earlier, js won't wait for the response from the server(the technical wording is 'the thread doesn't get blocked by network request'.) So basically you're returning undefined from this function.
Also see this video by JavaBrains. He uses an excellent analogy to understand async tasks in js, and you might also learn about what the event loop is and about single threaded-ness of javascript.
https://www.youtube.com/watch?v=EI7sN1dDwcY
Now the Second part (I really wanna go pee):
Replace(I've pointed out where I have made changes)
Your component file with this.
import React, { useState } from 'react';
import { fetchDataAction, fetchRequests } from './actions/fetchData';
import { connect } from 'react-redux';
export class CounterApp extends React.Component {
constructor(props) {
super(props);
}
btnClick = () => {
return this.props.fetchDataAction;
}
render() {
return (
<div className="App">
<h1>Hello</h1>
<div>
<h1>Fetch Data click below</h1>
<button onClick={() => fetchDataAction()}> //this is the only change here
Fetch Data
</button>
{this.props.datafetchedFromApi}
</div>
</div>
)
}
}
const mapStateToProps = state => ({
datafetchedFromApi: state.data
});
const mapDispatchToProps = (dispatch) => ({
fetchDataAction: () => dispatch(fetchDataAction())
});
export default connect(mapStateToProps, mapDispatchToProps)(CounterApp);
And then in your action.js file:
import axios from 'axios';
const requestURL='JSONUrl';
let responseData = '';
export function fetchRequests() {
return axios.get(requestURL) //change is here
//removed return responseData and the 'then' block
}
export const fetchDataAction = () => {
return dispatch => { //almost whole of this function is changed and that's it
fetchRequests().then((res) => {
responseData = res;
dispatch({
type: 'FETCH_DATA',
data: responseData
})
});
};
}
export function fetchDataSuccessAction(err) {
return {
type: 'FETCH_DATA_SUCCESS',
err
};
}
export function fetchDataErrorAction(err) {
return {
type: 'FETCH_DATA_ERROR',
err
};
}
Now it should work. Tell me more if it doesn't. Remember this answer is assuming that you have all your functions imported properly into your files. I'll be making edits if this doesn't answer your question.
So the answer was to use 'thunk' - an official "async function middleware" for redux.
Also see this to learn more about handling 'async actions' and also using redux thunk:
https://redux.js.org/tutorials/fundamentals/part-6-async-logic
https://www.npmjs.com/package/redux-thunk
Try this
<button onClick={() => this.props.fetchDataAction()}>
Also if you need to dispatch the url from your components you can do this way
<button onClick={() => this.props.fetchDataAction(url)}>
const mapDispatchToProps = (dispatch) => {
return {
fetchDataAction: (url) => dispatch(fetchRequests(url))
};
};
And in Action.js
export function fetchRequests(url) {
axios.get(url)
.then((res) => {
responseData = res;
});
return responseData;
}
<button onClick={() => this.props.fetchDataAction()}>
<button onClick={() => this.props.fetchDataAction}>
Here you are trying to return the function definition alone ,not call the fetchDataAction onclick use onClick={() => this.props.fetchDataAction()} or pass a separate handler for the onclick as good practice.
For the other issue you have mentioned TypeError: this.props.fetchDataAction is not a function is because of the curly brackets used while importing fetchRequests
Remove the curly brackets
import fetchRequests from "./actions/fetchData";
This should resolve your issue

Fetching data with redux-thunk middle ware

I am learning react-redux async actions with redux-thunk, I would like to fetch data from API (my local database), Unfortunately fetching data with redux-thunk middleware data is not fetched but without thunk middleware data is fetched.
So here are action creators with thunk middleware, which is not working
// retriev comments
export const fetchComments= () =>{
return dispatch =>{
dispatch(fetchCommentsRequest);
axios.get('/api/v1/todo')
.then(response =>{
const comments =response.data;
dispatch(fetchCommentsSucces(comments))
})
.catch(error =>{
const erroMsg =errors.messages
dispatch(fetchCommentsFailure(error))
})
}
}
And here is console log result :
Here is a component where I am calling the function to fetch data from API,
import React, {useEffect}from 'react'
import { fetchComments} from '../store/actions'
import { connect } from "react-redux";
function Dashboard(userComments) {
useEffect(() =>{
fetchComments();
}, [])
return (
<div>
<p>Fetching data</p>
</div>
)
}
const mapStateToProps = state => {
console.log("I am state", state);
return {
isAuthenticated: state.Auth.isAuthenticated,
user: state.Auth.user,
userComments: state.comments
};
};
const mapDispatchToProps = dispatch => {
return {
fetchComments: () => dispatch(fetchComments()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
The whole store can be found here: store
Can someone tells me why data is not fetched?
There is an issue with how fetchComments method is called inside the <Dashboard> component.
Once a React component is connected to a Redux store, the data from the store (mapStateToProps) and the functions it can use to dispatch actions to the store (mapDispatchToProps) are passed to that component as an object.
The <Dashboard> component receives this props object that can be accessed inside it like:
function Dashboard(props) {
useEffect(() =>{
props.fetchComments();
}, [])
return (
<div>
<p>Fetching data</p>
</div>
)
}
or using destructuring:
function Dashboard({ isAuthenticated, user, userComments, fetchComments }) {
useEffect(() =>{
fetchComments();
}, [])
return (
<div>
<p>Fetching data</p>
</div>
)
}
In your thunk, dispatch the action properly i.e. call the fetchCommentsRequest function (you are providing reference)
export const fetchComments= () =>{
return dispatch =>{
dispatch(fetchCommentsRequest()); //<-----call the fuction
axios.get('/api/v1/todo')
.then(response =>{
const comments =response.data;
dispatch(fetchCommentsSucces(comments))
})
.catch(error =>{
const erroMsg =errors.messages
dispatch(fetchCommentsFailure(error))
})
}
}
In your repo, the fetchCommentsSucces needs to take an argument.
export function fetchCommentsSucces(comments){ //<----pass argument i.e comments
console.log('success')
return{
type: ActionTypes.FETCH_COMMENTS_SUCCESS,
payload: comments //<----provide correct payload
}
}

Why list doesn't appear on the page? What are the errors in my React-Redux (Api) application? And how to fix them?

First, I made a small application on the React.js. Using the fetch method, I take the API
And these are the main files of my application:
Index.js:(action)
export const SHOW_AIRPLANES = "SHOW_AIRPLANES";
export function showAirplanes() {
return (dispatch, getState) => {
fetch("https://api.iev.aero/api/flights/25-08-2019").then(response => {
dispatch({ type: SHOW_AIRPLANES, payload: response.data });
});
};
}
airplanes.js:(reducer)
import { SHOW_AIRPLANES } from '../actions'
const initialState = {
list: []
}
export function showAirplanes(state = initialState, action) {
switch (action.type) {
case SHOW_AIRPLANES:
return Object.assign({}, state, {list: action.payload})
default:
return state
}
}
index.js(reducer):
import { combineReducers } from "redux";
import { showAirplanes } from "./airplanes";
const rootReducer = combineReducers({
user: showAirplanes
});
export default rootReducer;
First, you should use the createStore function like so:
const initialData = {}; // whatever you want as initial data
const store = createStore(reducers, initialData, applyMiddleware(thunk));
Then pass it to your provider
<Provider store={store}>
{...}
</Provider
next, when you map your reducers inside the combineReducers function, each key in this object represents a piece of your state. So when you do user: showAirplanes it means that you intend to use it in the mapStateToProps with state.user.list so I think you meant to call it airplane: showAirplanes.
Then, your reducer name is not informative enough, I would suggest to change it to airplanesReducer.
Next issue, the call to fetch returns a response that has JSON that must be resolved.
Change this:
fetch("https://api.iev.aero/api/flights/25-08-2019").then(response => {
dispatch({ type: SHOW_AIRPLANES, payload: response.data });
});
To this:
fetch("https://api.iev.aero/api/flights/25-08-2019")
.then(res => res.json())
.then(response => {
dispatch({ type: SHOW_AIRPLANES, payload: response.body.departure });
});
Note that I've changed the value that you need to resolve from the response as well.
Inside your App.js component you need to create a constructor and bind the renderAirplaneList function to this
// Inside the App class
constructor(props) {
super(props);
this.renderAirplaneList = this.renderAirplaneList.bind(this);
}
And finally (I hope I didn't miss anything else), you map your state in the App.js component to { airplanes: state.airplanes.list} so the name of the prop you expect inside your component is props.airplanes.
renderAirplaneList() {
if (!this.props.airplanes.length) {
return null;
}
const arr = this.props.airplanes || [];
return arr.map(airplane => {
return (
<tr key={airplane.id}>
<td>{airplane.ID}</td>
<td>{airplane.term}</td>
<td>{airplane.actual}</td>
<td>{airplane["airportToID.city_en"]}</td>
</tr>
);
});
}
Make sure you go over the documentation of React and Redux, they have all the information you need.
Good luck.
aren't you suppose to send some parameters to this call?
this.props.showAirplanes()
it seems that it has 2 parameters: state and action, although state seems to have already it's default value

How can I refactor this ASYNC call in my react component to make it more readable?

I want my component to fetch an array of objects from the server. Each object is a message with author, body and date. I then want to render these messages in my react component.
My react component currently fetches data from the server before mounting. It will then store this message list in the redux state.|
I'm sure there's a better way of writing this code.
1. Can I place the fetch request in either the Action or Reducer file?
2. Can I write a function in the component to make the async call?
import React, { Component } from 'react';
import Message from '../components/message.jsx';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
// Actions
import { fetchMessages } from '../actions/actions_index.js';
class MessageList extends Component {
constructor(props) {
super(props)
}
componentWillMount() {
fetch('https://wagon-chat.herokuapp.com/general/messages')
.then(response => response.json(),
error => console.log('An error occured receiving messages', error))
.then((data) => {
this.props.fetchMessages(data.messages);
});
}
render() {
return (
<div className="message-list">
{this.props.messageList.map( (message, index) => { return <Message key={index} message={message}/> })}
</div>
)
}
}
function mapStateToProps(state) {
return {
messageList: state.messageList
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{ fetchMessages: fetchMessages },
dispatch
)
}
export default connect(mapStateToProps, mapDispatchToProps)(MessageList);
Can I place the fetch request in either the Action or Reducer file?
The fetch request should be placed in action creator. Where the retrieved data will be dispatched to reducer later to manipulate the data, and lastly update the store to show on UI. Here's simple flow for most of react-redux app.
UI -> Action creator (calling request, saga etc..) -> reducer -> store -> UI
Can I write a function in the component to make the async call?
Yes, this should be called action creator, and you can see actions.js below for more reference.
I think you can safely follow this sample pattern where most tutorials out there apply. I'm assuming all files listed here are in the same directory.
constant.js
const MESSAGE_FETCH__SUCCESS = 'MESSAGE/FETCH__SUCCESS'
const MESSAGE_FETCH__ERROR = 'MESSAGE/FETCH__ERROR'
export {
MESSAGE_FETCH__SUCCESS,
MESSAGE_FETCH__ERROR
}
actions.js
import {
MESSAGE_FETCH__SUCCESS,
MESSAGE_FETCH__ERROR
} from './constant';
const fetchMessageError = () => ({
type: MESSAGE_FETCH__ERROR
})
const fetchMessageSuccess = data => ({
type: MESSAGE_FETCH__SUCCESS,
payload: data
})
const fetchMessages = () => {
const data = fetch(...);
// if error
if (data.error)
fetchMessageError();
else fetchMessageSuccess(data.data);
}
export {
fetchMessages
}
reducers.js
import {
MESSAGE_FETCH__SUCCESS,
MESSAGE_FETCH__ERROR
} from './constant';
const INIT_STATE = {
messageList: []
}
export default function( state = INIT_STATE, action ) {
switch(action.type) {
case MESSAGE_FETCH__SUCCESS:
return {
...state,
messageList: action.payload
}
case MESSAGE_FETCH__ERROR:
// Do whatever you want here for an error case
return {
...state
}
default:
return state;
}
}
index.js
Please read the comment I noted
import React, { Component } from 'react';
import Message from '../components/message.jsx';
import { connect } from 'react-redux';
// Actions
import { fetchMessages } from './actions';
class MessageList extends Component {
/* If you don't do anything in the constructor, it's okay to remove calling `constructor(props)`
*/
//constructor(props) {
// super(props)
//}
// I usually put this async call in `componentDidMount` method
componentWillMount() {
this.props.fetchMessage();
}
render() {
return (
<div className="message-list">
{
/* Each message should have an unique id so they can be used
for `key` index. Do not use `index` as an value to `key`.
See this useful link for more reference: https://stackoverflow.com/questions/28329382/understanding-unique-keys-for-array-children-in-react-js
*/
this.props.messageList.map( message => <Message key={message.id} message={message}/> )
}
</div>
)
}
}
function mapStateToProps(state) {
return {
messageList: state.messageList
}
}
export default connect(mapStateToProps, {
fetchMessages
})(MessageList);
You could use redux-thunk in an action called getMessages.
So:
(The double arrow func, is to return an action, see redux-thunk)
const getMessages = ()=>(dispatch, getState)=>{
fetch('https://wagon-chat.herokuapp.com/general/messages')
.then(response => response.json(),
error => dispatch(['error', error]))
.then((data) => {
dispatch(data);
})
}
Then you've successfully reduced your component to:
componentWillMount(){
this.props.getMessages()
}
I think #Duc_Hong answered the question.
And in my opinion, I suggest using the side-effect middle-ware to make AJAX call more structured, so that we could handle more complicated scenarios (e.g. cancel the ajax request, multiple request in the same time) and make it more testable.
Here's the code snippet using Redux Saga
// Actions.js
const FOO_FETCH_START = 'FOO\FETCH_START'
function action(type, payload={}) {
return {type, payload};
}
export const startFetch = () => action{FOO_FETCH_START, payload);
// reducer.js
export const foo = (state = {status: 'loading'}, action) => {
switch (action.type) {
case FOO_FETCH_STARTED: {
return _.assign({}, state, {status: 'start fetching', foo: null});
}
case FOO_FETCH_SUCCESS: {
return _.assign({}, state, {status: 'success', foo: action.data});
}
......
}
};
Can I place the fetch request in either the Action or Reducer file?
// Saga.js, I put the ajax call (fetch, axios whatever you want) here.
export function* fetchFoo() {
const response = yield call(fetch, url);
yield put({type: FOO_FETCH_SUCCESS, reponse.data});
}
// This function will be used in `rootSaga()`, it's a listener for the action FOO_FETCH_START
export function* fooSagas() {
yield takeEvery(FOO_FETCH_START, fetchFoo);
}
Can I write a function in the component to make the async call?
// React component, I trigger the fetch by an action creation in componentDidMount
class Foo extends React.Component {
componentDidMount() {
this.props.startFetch();
}
render() {
<div>
{this.props.foo.data ? this.props.foo.data : 'Loading....'}
<div>
}
}
const mapStateToProps = (state) => ({foo: state.foo});
const mapDispatchToProps = { startFetch }
export default connect(mapStateToProps, mapDispatchToProps) (Foo);
//client.js, link up saga, redux, and React Component
const render = App => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
combinedReducers,
initialState,
composeEnhancers(applyMiddleware(sagaMiddleware))
);
store.runSaga(rootSaga);
return ReactDOM.hydrate(
<ReduxProvider store={store}>
<BrowserRouter><AppContainer><App/></AppContainer></BrowserRouter>
</ReduxProvider>,
document.getElementById('root')
);
}

Thunk Action Body not being Invoked

My thunk action doesn't seem to be running through its core logic. I tall the thunk action from componentDidMount but it doesn't in turn cause this to run: const response = await findOne(id).
Also, I thought I didn't need to explicitely pass dispatch as a prop to mapDispatchToProps if using redux-thunk, I thought that the way I have my thunk setup is that dispatch is available already to the thunk? And I've used other actions like this and it's worked fine, why not this one?
Thunk Action
export function fetchCompany(id) {
return async (dispatch) => {
try {
const response = await findOne(id)
if(response && response.body) {
const company = response.body
dispatch(companyReceived(company))
}
} catch(err) {
console.log("failed request in authenticate thunk action")
console.log(`error details: ${err.status} /n ${err}`)
}
}
}
Container
......
import { fetchCompany } from '../../client/actions/company/CompanyAsyncActions'
class InterviewContainer extends Component {
async componentDidMount() {
await fetchCompany(this.props.params.companyId)
}
render(){
return (this.props.company && <Interview className='ft-interview' company={this.props.company} />)
}
}
const mapStateToProps = state => ({
company: state.company.company
})
const mapDispatchToProps = {
fetchCompany: fetchCompany
}
export default connect(mapStateToProps, mapDispatchToProps)(InterviewContainer)
In the past, I haven't passed (dispatch) as a prop to mapDispatchToProps and it worked fine. But I see everyone else is doing so. How was my code working in the past if I wasn't doing that? And why isn't this working this time around in the example above?
Taking a look at another async action thunk container and call example, this is working completely fine, and I'm calling it the same way in another container
container
class HomePageContainer extends Component {
constructor(){
super()
}
async componentDidMount() {
await this.props.fetchFeaturedCompanies()
await this.props.fetchCompanies()
await this.props.fetchCountries()
}
render(){
return (<HomePage className='ft-homepage'
featuredCompanies={this.props.featuredCompanies}
countries={this.props.countries}
companies={this.props.companies}
/>)
}
}
const mapStateToProps = state => ({
countries: state.country.countries,
companies: state.company.companies,
featuredCompanies: state.company.featuredCompanies
})
const mapDispatchToProps = {
fetchCountries: fetchCountries,
fetchCompanies: fetchCompanies,
fetchFeaturedCompanies: fetchFeaturedCompanies
}
export default connect(mapStateToProps, mapDispatchToProps)(HomePageContainer)
thunk action
export function fetchCompanies() {
return async (dispatch, getState) => {
const response = await find()
if(response && response.body) {
const companies = response.body
dispatch(companiesReceived(companies))
}
}
}
In componentDidMount of InterviewContainer you're accidentally calling the imported fetchCompany, instead of this.props.fetchCompany.

Categories