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.
Related
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.
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);
})
}
I need help. I don't understand why my dispatch action doesn't work. I've redux store currency list and current currency.
My reducer:
export const currencyReducer = (
state: typeState = initialState,
action: TypeActionCurrency
): typeState => {
switch (action.type) {
case types.CURRENCY_FILL_LIST:
return { ...state, list: action.payload }
case types.CURRENCY_SET_CURRENT:
return {
...state,
current:
state.list.find(currency => currency._id === action.payload) ||
({} as ICurrency),
}
default:
return state
}
}
My actions:
export const setCurrencyList = (currencies: ICurrency[]) => ({
type: types.CURRENCY_FILL_LIST,
payload: currencies,
})
export const setCurrentCurrency = (_id: string) => ({
type: types.CURRENCY_SET_CURRENT,
payload: _id,
})
My useEffect:
useEffect(() => {
if (!list.length) {
const fetchCurrencies = async () => {
try {
const data = await $apiClient<ICurrency[]>({ url: '/currencies' })
dispatch(setCurrencyList(data))
if (!current._id) dispatch(setCurrentCurrency(data[0]._id))
} catch (error) {
console.log(error)
}
}
fetchCurrencies()
}
}, [])
I want make request when load page and write currency list to Redux store, if we don't have current currency we write default currency from data.
There is one more strange thing, my redux extension shows that the state has changed, but when I receive it via the log or useSelector, it is empty
enter image description here
Thanks!
I am not 100% sure but it should work.
const [loader, setLoader] = useState(false);
const list = useSelector(state => state.list)
useEffect(() => {
if (!list.length) {
const fetchCurrencies = async () => {
try {
setLoader(true)
const data = await $apiClient<ICurrency[]>({ url: '/currencies' })
dispatch(setCurrencyList(data))
if (!current._id) dispatch(setCurrentCurrency(data[0]._id))
} catch (error) {
console.log(error)
} finally {
setLoader(false)
}
}
fetchCurrencies()
}
}, [])
useEffect(() => {
console.log(list);
}, [loader])
I want to add a data to the restfull api by action.
But I get this error.
export const GlobalContext = createContext();
const GlobalProvider = ({ children }) => {
const [userData, setUserData] = useState([]);
const [meetings, setMeetings] = useState([]);
useEffect(() => {
fetch('http://localhost:4000/users')
.then(res => res.json())
.then(data => {
setUserData(data);
dispatch({
type: 'CREATE_MEETING',
paylaod: data
})
});
fetch('http://localhost:4000/meeting')
.then(res => res.json())
.then(data => setMeetings(data));
}, []);
const [state, dispatch] = useReducer(AppReducer, meetings);
//Actions
const updateProfile = (id) => {
dispatch({
type: 'UPDATE_PROFILE',
payload: id
})
};
const createMeeting = (meeting) => {
dispatch({
type: 'CREATE_MEETING',
paylaod: meeting
})
};
return (
<GlobalContext.Provider value={{
meeting: meetings, userData, createMeeting
}}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
{children}
</MuiPickersUtilsProvider>
</GlobalContext.Provider>
)
}
export default GlobalProvider
reducer
const reducer = (state, action) => {
switch (action.type) {
case 'CREATE_MEETING':
return {
meeting: [action.payload, ...state.meetings]
}
default:
return state;
}
}
export default reducer;
How can I add data to the api with fetch?
case 'CREATE_MEETING':
console.log(state)
return [...state,
fetch('http://localhost:4000/meeting', {
method: 'POST',
headers: { "Content-type": "Application/json" },
body: JSON.stringify(state)
})
]
could you help me please?
As explained in Spreading undefined in array vs object you get a TypeError when trying to spread undefined.
Either wrap your setMettings in a conditional:
data => {
if (data) {
setMeetings(data)
}
}
Or provide a default for state.mettings in your reducer:
const reducer = (state, action) => {
switch (action.type) {
case 'CREATE_MEETING':
return { meeting: [action.payload, ...(state.meetings || [])] }
}
}
I have this reducer which shall return all comments on the page :
case actionTypes.GET_COMMENT:
return {
...state,
comments: action.comments
}
export const getComment = (comments : Object[]) => {
return {
type : actionTypes.GET_COMMENT,
comments
}
}
Here is how i call it in component
useEffect(() => {
const getAllCommentsOnCurrentPostFromBE = (id: Number) => {
axios.get(`http://localhost:4000/getComment/${id}`)
.then(res => {
console.log('--------res,get', res.data);
dispatch(actions.getComment(res.data))
console.log('--------posts', posts);
})
.catch(err => {
console.log('--------err', err);
})
}
getAllCommentsOnCurrentPostFromBE(grabIdFromLocation())
},[])
res.data is collection of key value pairs like this {"comment":"123"}
But it is not rendering anything,any suggestions please?
There is no dispatch() function. Downloaded data do not pass to the reducer. You have to use redux-thunk to use async functions with redux.
I recommend using actions in separate files:
export const fetchDataFromDatabase = () => async (
disapatch,
getState,
) => {
const response = await axios.get();
disapatch({
type: TYPE,
data: response.data,
});
};
Then export your component export default connect(yourProps,{fetchDataFromDatabase})(YourComponent)
In your component you can call props.fetchDataFromDatabase()