useReducer with axios to pull data - reactjs

Sorry if this is a stupid question, I'm new to properly trying to understand React.
Here's what I'm working with:
import React, { useReducer } from "react";
import axios from "axios";
export const ACTIONS = {
ADD_STANDARD: 'add-to-compare',
REMOVE_STANDARD: 'remove-from-compare'
}
function reducer(standards, action) {
switch(action.type) {
case(ACTIONS.ADD_STANDARD):
return [...standards, addCompare(action.payload.standard)]
case(ACTIONS.REMOVE_STANDARD):
return standards.filter(item => item.id !== action.payload.standard)
default:
return 'Nothing to add'
}
}
function addCompare( standard ) {
return axios
.get("https://the-url.com" + standard)
.then((res) => {
console.log(res.data)
return {
key: res.data.id,
title: res.data.title.rendered
}
})
.catch((err) => console.log(err));
}
export default function EntryStandards() {
const [standards, dispatch] = useReducer(reducer, []);
const addStandards = function(id) {
dispatch({ type: ACTIONS.ADD_STANDARD, payload: {standard: id}})
}
return (
<>
<button onClick={() => addStandards(9603)}>Add 9603!</button>
<button onClick={() => addStandards(9567)}>Add 9567!</button>
<button onClick={() => addStandards(9531)}>Add 9531!</button>
<button onClick={() => addStandards(9519)}>Add 9519!</button>
{standards.map(standard => {
return <p><button onClick={() => dispatch({ type: ACTIONS.REMOVE_STANDARD, payload: { standard: standard.id } })}>X</button> { standard.title } - { standard.version }</p>
})}
</>
)
}
As you can see, I have a button currently which has a hard-coded ID. When clicked, that button triggers a dispatch on a useReducer which performs an API data lookup using Axios against WordPress' Rest API.
If I console log inside the Axios return, I see the data. The return straight after where I create my object however simply returns an empty object into the reducer.
Is the dispatcher not able to use Axios in this way?
Is there a better way to go about this?
As always, your help is appreciated.
Ben

addCompare is an async function. So you can't use it directly in a reducer as a reducer must return a result synchronously.
You may want to change your code to launch you request and then use the reducer to add the data like below:
import React, { useReducer } from "react";
import axios from "axios";
export const ACTIONS = {
ADD_STANDARD: 'add-to-compare',
REMOVE_STANDARD: 'remove-from-compare'
}
function reducer(standards, action) {
switch(action.type) {
case(ACTIONS.ADD_STANDARD):
return [...standards,action.payload.standard]
case(ACTIONS.REMOVE_STANDARD):
return standards.filter(item => item.id !== action.payload.standard)
default:
return 'Nothing to add'
}
}
function addCompare( standard ) {
return axios
.get("https://the-url.com" + standard)
.then((res) => {
console.log(res.data)
return {
key: res.data.id,
title: res.data.title.rendered
}
})
.catch((err) => console.log(err));
}
export default function EntryStandards() {
const [standards, dispatch] = useReducer(reducer, []);
const addStandards = async function(id) {
const standard = await addCompare(id)
dispatch({ type: ACTIONS.ADD_STANDARD, payload: { standard }})
}
return (
<>
<button onClick={() => addStandards(9603)}>Add 9603!</button>
<button onClick={() => addStandards(9567)}>Add 9567!</button>
<button onClick={() => addStandards(9531)}>Add 9531!</button>
<button onClick={() => addStandards(9519)}>Add 9519!</button>
{standards.map(standard => {
return <p><button onClick={() => dispatch({ type: ACTIONS.REMOVE_STANDARD, payload: { standard: standard.id } })}>X</button> { standard.title } - { standard.version }</p>
})}
</>
)
}

Thanks Gabriel for your insight on async. It doesn't feel like a tidy solution, but I've ended up making the reducer store only the ID of the post, and then importing a new component that takes the ID and performs the Axios stage of mapping the data. Thanks for your help with this.

Related

Can't use multiple dispatch in react redux

I have a problem, so I am trying to put two or more "dispatch" in my application
but I don't know why it is just working one dispatch that I put in last
import axios from "axios";
const setDataBlog = (page) =>{
return (dispatch) => {
axios.get(`http://localhost:4000/api/blogs/?page=${page}&perPage=3`)
.then(result => {
const responseAPI = result.data
dispatch({type: 'UPDATE_PAGE', payload:
{currentPage: responseAPI.current_page,
totalPage: responseAPI.total_page}}) // this is not working
dispatch({type: 'SET_BLOGS', payload: responseAPI.data}) //just work in here
})
.catch(error => {
console.log('error',error);
})
}}
export default setDataBlog
but if I change the location of the dispatch
import axios from "axios";
const setDataBlog = (page) =>{
return (dispatch) => {
axios.get(`http://localhost:4000/api/blogs/?page=${page}&perPage=3`)
.then(result => {
const responseAPI = result.data
dispatch({type: 'SET_BLOGS', payload: responseAPI.data}) //not working
dispatch({type: 'UPDATE_PAGE', payload:
{currentPage: responseAPI.current_page,
totalPage: responseAPI.total_page}}) // working
})
.catch(error => {
console.log('error',error);
})
}}
export default setDataBlog
I'm trying to use it here
import { useEffect} from "react";
import CardBlog from "../components/CardBlog";
import Pagination from "../components/Pagination";
import {useDispatch, useSelector} from 'react-redux'
import {setDataBlog} from '../redux/action'
const Home = () => {
const {dataBlog, page} = useSelector(state => state.homeReducer);
const dispatch = useDispatch();
//check working or not
console.log('page', page);
useEffect(() => {
dispatch(setDataBlog(1))
}, [dispatch])
return (
<div className="max-w-screen-xl mx-auto px-4 py-16 sm:px-6 lg:px-8">
<div className=" md:grid md:grid-cols-2 lg:grid-cols-3">
{dataBlog?.map(blog => (
<CardBlog key={blog._id} image={`http://localhost:4000/${blog.image}`}
title={blog.title}
body={blog.body}
author={blog.author}
date={blog.createdAt}/>
))}
</div>
<div>
<Pagination/>
</div>
</div>
);
}
export default Home;
thanks, sorry for my bad English, but I hope you understand what I said
I can't tell you why your code is failing, but I'd like to offer some advice. Avoid firing multiple synchronous actions.
Think of an action as representing single thing that has happened: often a user event such as a button click or key press, or in this case a network response.
I recommend combining the two actions above into a single action, e.g.
dispatch({
type: 'BLOG_API_RESPONSE',
payload: {
currentPage: responseAPI.current_page,
totalPages: responseAPI.total_page,
data: responseAPI.data,
},
});
You can hook into BLOG_API_RESPONSE at multiple places in your reducers. Actions to state updates don't have to be one-to-one. One action can produce many state updates.
You'll find your code easier to rationalise and debug when you restrict yourself to firing single synchronous actions.
Try this:
const setDataBlog = (page) => async dispatch => {
try {
const { responseAPI } = await axios.get(`http://localhost:4000/api/blogs/?page=${page}&perPage=3`)
dispatch({type: 'SET_BLOGS', payload: responseAPI.data})
dispatch({type: 'UPDATE_PAGE', payload: {currentPage:responseAPI.current_page, totalPage: responseAPI.total_page}})})
catch(error => {
console.log('error',error);
})
}

React-Redux Update Form (PUT request) issue

I am trying to update a form but something is not working as it should. After I click Update, the updated information is logged in the console, but it seems that the Redux side of the state management is not working. I am not getting any errors in the console, but neither my action UPDATE_POST is visible in Redux Dev Tools on Chrome.
Here is the code:
The UpdateForm component:
import { useState , useEffect} from "react";
import { useHistory, useParams } from 'react-router-dom';
import jsonPlaceholder from "../apis/jsonPlaceholder";
import {updatePost} from '../actions'
import { useDispatch } from 'react-redux';
const UpdateForm = () => {
const dispatch = useDispatch()
const history = useHistory();
const { id } = useParams();
const [post, setPost] = useState({});
const [title, setTitle] = useState(post.title);
const [body, setBody] = useState(post.body);
const [author, setAuthor] = useState(post.author);
const fetchPost = async () => {
const response = await jsonPlaceholder.get(`/posts/${id}`)
console.log(response.data)
setPost(response.data)
setTitle(response.data.title)
setBody(response.data.body)
setAuthor(response.data.author)
return response.data
}
useEffect(() => {
fetchPost();
}, [])
const handleUpdate = async (e) => {
e.preventDefault();
const post = { title, body, author }
dispatch(updatePost(post))
console.log('post', post)//updated post is logged in console
history.push('/')
}
console.log("title", title)
return (
<div className="create">
<h2>Update Blog</h2>
<form>
<label>Blog title:</label>
<input
type="text"
required
defaultValue={title}
onChange={(e) => setTitle(e.target.value)}
/>
<label>Blog body:</label>
<textarea
required
defaultValue={body}
onChange={(e) => setBody(e.target.value)}
></textarea>
<label>Author:</label>
<input
type="text"
required
defaultValue={author}
onChange={(e) => setAuthor(e.target.value)}
/>
<button onClick={handleUpdate}>Update</button>
</form>
</div>
);
}
export default UpdateForm;
The action:
export const updatePost = (post) => async dispatch => {
const res = await jsonPlaceholder.put(`posts/update/${post._id}`);
dispatch({
type: UPDATE_POST,
payload: res.data
})
}
And the reducer:
import { ADD_POST, DELETE_POST, UPDATE_POST } from '../actions/types';
const postReducer = (state = [], action) => {
switch (action.type) {
case ADD_POST:
return state.concat([action.data]);
case UPDATE_POST:
return {
...state,
post: action.data
}
case DELETE_POST:
return state.filter((post)=>post.id !== action.id);
default:
return state
}
}
export default postReducer;
Here is the node.js/express server side of the request:
router.put('/update/:id', async (req, res) => {
try {
let post = await Post.findOneAndUpdate(req.params.id, {
title: req.body.title,
body: req.body.body,
author: req.author.body
})
console.log('server', post)
return res.json(post)
} catch (error) {
console.error(error.message);
res.status(500).send('Server Error')
}
})
I am now getting server error (500), and if I remove the line author: req.author.body, I am not getting the error. The code on the front still does not work.
As I see you are directly calling your actions instead of dispatching it
import useDispatch and use it like this
import { useDispatch } from "react-redux";
UpdateForm.js
const UpdateForm = () => {
....
const dispatch = useDispatch();
.....
const handleUpdate = async (e) => {
e.preventDefault();
const post = { title, body, author }
dispatch(updatePost(post)) // dispatch like this
console.log('post', post)//updated post is logged in console
history.push('/')
}
console.log("title", title)
return (
<div className="create">
.......
</div>
);
}
export default UpdateForm;
reducer
instead of action.payload, you're accessing action.data
case UPDATE_POST:
return {
...state,
post: action.payload
}
You need to dispatch the updatePost action, not call it directly. You're missing useDispatch call.
Here's a link to React Redux documentation covering it:
https://react-redux.js.org/api/hooks#usedispatch
Example:
import React from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch({ type: 'increment-counter' })}>
Increment counter
</button>
</div>
)
}
[UPDATE]
Just noticed that your updatePost action is an higher order function so once you add the call to useDispatch you'll need to change the call to updatePost from
updatePost(post)
to
updatePost(post)(dispatch)
To be honest I would probably go with a book action creator and move the API call to the component itself. If you're interested in async actions I would suggest looking into react-thunk, it is fairly easy to begin with.
[UPDATE 2]
There seem to be a typo in the express code.
req.author.body
should be
req.body.author
[UPDATE 3]
The post object in the updatePost does not contain the _id field (check your handleUpdate function) thus you're getting the url: "posts/update/undefined".

Redux store not getting updated

I need to display an nested array. But I am unable to display the nested list as my redux store is not getting updated. Below is the sample of the structure of the data:
{
email: "fgh#gmail.com"
tId: 2
teacherClasses: null
teacherUserRef: 3
user:
{
admin: null
firstName: "fgh"
id: 3
lastName: "fgh"
}}
I am unable to display anything which is inside user.
below is the code:
Reducer:
import { ACTION_TYPES } from "../actions/teacher";
const initialState = {
list: []
}
export const teacher = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPES.FETCH_ALL:
return {
...state,
list: [
...action.payload]
}
case ACTION_TYPES.FETCHBYID:
return {
...state,
list: [action.payload]
}
case ACTION_TYPES.CREATE:
return {
...state,
list: [...state.list, action.payload]
}
case ACTION_TYPES.UPDATE:
return {
...state,
list: state.list.map(x => x.id == action.payload.id ? action.payload : x)
}
case ACTION_TYPES.DELETE:
return {
...state,
list: state.list.filter(x => x.id != action.payload)
}
default:
return state
}
}
Component page:
Teacher.js:
const Teacher = ({ ...props }) => {
const [currentId, setCurrentId] = useState(0)
useEffect(() => {
console.log("teacher call")
props.fetchAllTeacher()
console.log(props.teacherList)
}, [currentId])//componentDidMount
return (
<div className="site-layout-background" style={{ padding: 24, textAlign: 'center' }}>
<Space direction="vertical" align="center">
<TableContainer>
<Table>
<TableHead >
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Email</TableCell>
<TableCell></TableCell>
</TableRow>
{
props.teacherList.map((record, index) => {
return (<TableRow key={index} hover>
<TableCell>{record.email}</TableCell>
<TableCell>{record.user.firstName}</TableCell>
<TableCell>
<ButtonGroup variant="text">
<Button icon={<DeleteOutlined />} onClick={() => onDelete(record.user.id)}></Button>
</ButtonGroup>
</TableCell>
</TableRow>)
})}
</TableHead>
<TableBody>
</TableBody>
</Table>
</TableContainer>
</Space>
</div>
);
}
const mapStateToProps = state => ({
teacherList: state.teacher.list,
userList: state.user.list
})
const mapActionToProps = {
fetchAllTeacher: actions.fetchAll,
deleteUser: actions1.Delete
}
export default connect(mapStateToProps, mapActionToProps)(Teacher);
Action creator:
import api from "./api";
export const ACTION_TYPES = {
CREATE: 'CREATE',
UPDATE: 'UPDATE',
DELETE: 'DELETE',
FETCH_ALL: 'FETCH_ALL',
FETCHBYID: 'FETCHBYID'
}
export const fetchAll = () => dispatch => {
api.teacher().fetchAll()
.then(response => {
dispatch({
type: ACTION_TYPES.FETCH_ALL,
payload: response.data
})
})
.catch(err => console.log(err))
}
export const fetchById = (id) => dispatch => {
api.teacher().fetchById(id)
.then(response => {
dispatch({
type: ACTION_TYPES.FETCHBYID,
payload: response.data
})
})
.catch(err => console.log(err))
}
export const create = (data, onSuccess) => dispatch => {
api.teacher().create(data)
.then(res => {
dispatch({
type: ACTION_TYPES.CREATE,
payload: res.data
})
onSuccess()
})
.catch(err => console.log(err))
}
export const update = (id, data, onSuccess) => dispatch => {
api.teacher().update(id, data)
.then(res => {
dispatch({
type: ACTION_TYPES.UPDATE,
payload: { id, ...data }
})
onSuccess()
})
.catch(err => console.log(err))
}
export const Delete = (id, onSuccess) => dispatch => {
api.teacher().delete(id)
.then(res => {
dispatch({
type: ACTION_TYPES.DELETE,
payload: id
})
onSuccess()
})
.catch(err => console.log(err))
}
I am getting an error saying firstName is undefined.
Please help.
Recommendation
Since you are using functional component, you should use react-redux hooks like useSelector, useDispatch.
import { useSelector, useDispatch } from "react-redux"
...
teacherList = useSelect(state => state.teacher.list)
userList = useSelect(state => state.user.list)
const dispatch = useDispatch()
...
{
dispatch(actions.fetchAll(...))
dispatch(actions1.Delete(...))
}
Problem
First, you don't need to set currentId as a dependency of useEffect.
When dependency is an empty list, the callback will only be fired once, similar to componentDidMount.
Second, fetchAllTeacher is an asynchronous action which means you need to wait until all teachers are fetched successfully.
So you need to add a lazy loading feature.
The reason that your redux store is not getting updated is because you must dispatch the actions. The correct signature for mapDispatchToProps is:
const mapDispatchToProps = (dispatch) => {
fetchAllTeacher: () => dispatch(actions.fetchAll()),
deleteUser: (id) => dispatch(actions.Delete(id)),
}
export default connect(mapStateToProps, mapDispatchToProps)(Teacher);
BUT the there is a better way. You are actually mixing two paradigms, and while the above will work, you should use redux hooks, since you have created a functional component and you are already using the useEffect hook.
It could work like this:
import { useSelector, useDispatch } from "react-redux"
const Teacher = ({ ...props }) => {
const dispatch = useDispatch();
useEffect(() => {
console.log("teacher call")
const teachers = props.fetchAllTeacher();
// dispatch the action that will add the list to the redux state
dispatch(actions.fetchAll(teachers));
}, [currentId]);
// fetch the teacher list from redux store
const teacherList = useSelector(state => state.teacher.list);
return (...);
}
Consider moving the selector definition state => state.teacher.list to its own module so that you can reuse it in other components and update it in one place if the structure of your store changes.
It looks like no actions were getting dispatched in your code, so the problem was not due to nesting of the data. You can have nested data in your state without a problem.

cannot dispatch async function when click button in react

I am developing a component library and want to use the host project's store. I want the component library could access the host project's store. so I use the hook useSelector, useDispath,I found that I could use normal dispatch, but not async dispatch.
import React from "react";
import { useSelector, useDispatch } from "react-redux";
export default function AccountInfo({ onAccountClick }) {
const name = useSelector((state) => state.account.name);
const balance = useSelector((state) => state.account.balance);
const dispatch = useDispatch();
const fetchImage = (url) => async () => {
try {
const res = await fetch(url, {
method: "GET",
});
const address = await res.json();
dispatch({ type: "GET_ACCOUNT_IMAGE", address });
} catch (err) {
console.log("error");
}
};
return (
<>
<ul>
<li>name: {name}</li>
<li>balance: {balance}</li>
</ul>
<button className="button" onClick={() => fetchImage("https://aws.random.cat/meow")}>
Click
</button>
</>
);
}
If I change onClick to :
<button className="button" onClick={() => dispatch({ type: "GET_CONSOLE" })}>
It can do the work. But I want it an async function, which didn't work.
How can I solve this?

Redux Thunk silently fails to update state

A child component has the following button code:
// SelectDonation.js
<button
onClick={(e) => {
e.preventDefault();
this.props.testThunk();
console.log(store.getState());
}}
>Test thunks</button>
this.props.testThunk() does not update the state object. I connected Redux Thunk like so:
// reducer.js
import ReduxThunk from "redux-thunk";
const starting_state = {
log_to_console : 0,
donation_amount : 12,
checkoutStep : 'selectDonation',
};
const reducer = (previous_state = starting_state, action) => {
switch (action.type) {
case 'thunkTest':
return {
...previous_state,
redux_thunk_test_var : action.payload
};
default:
return previous_state;
}
};
export default createStore(reducer, starting_state, applyMiddleware(ReduxThunk));
I expect a new state property redux_thunk_test_var to display in state but it does not onClick. I do see the state variables with initial states in the console though.
Am I not passing down the thunk correctly? Here is App.js
// App.js
{this.props.checkoutStep === checkoutSteps.selectDonation &&
<SelectDonation
dispatch_set_donation_amount = {this.props.dispatch_set_donation_amount}
dispatchChangeCheckoutStep={this.props.dispatchChangeCheckoutStep}
{...this.props}
/>
}
</Modal>
</header>
</div>
);
}
}
const map_state_to_props = (state) => {
return {
log_prop : state.log_to_console,
donation_amount : state.donation_amount,
checkoutStep : state.checkoutStep,
}
};
const map_dispatch_to_props = (dispatch, own_props) => {
return {
dispatch_set_donation_amount : amount => dispatch(set_donation_amount(amount)),
dispatchChangeCheckoutStep : newStep => dispatch(changeCheckoutStep(newStep)),
dispatchUpdateStateData : (stateData, stateVariable) => (dispatch(updateStateData(stateData, stateVariable))),
testThunk
}
};
The action thunk:
// actions.js
export const testThunk = () => {
const testDelay = setTimeout(() => 'Set Timeout done', 2000);
return (dispatch) => {
testDelay.then((data) => dispatch({
type: 'thunkTest',
payload: data })
)
}
};
You need to dispatch the result of the testThunk() action creator. Right now, you're just returning it, and not calling dispatch(testThunk()).
See this gist comparing syntaxes for dispatching to help understand the issue better.
The best way to fix this is to use the "object shorthand" form of mapDispatch. As part of that, I suggest changing the prop names to remove the word "dispatch", which lets you use the simpler ES6 object literal syntax:
const map_dispatch_to_props = {
set_donation_amount,
changeCheckoutStep,
updateStateData,
testThunk,
};
conponentDidMount() {
this.props.testThunk();
}
const map_dispatch_props = {
testThunk
}
//action creator
const fetch = (data) => ({
type: 'thunkTest',
payload: data
})
const fakeFetch = () => new Promise((resolve, reject) => setTimeout(() => resolve('Set Timeout done'), 2000));
export const testThunk = () => (dispatch) => fakeFetch.then(data => dispatch(fetch(data)))

Resources