Problem when maping array - React with Redux Toolkit - javascript

I have the following problem: I use a fatch API to get all the products I have registered in my database (mongodb), then I store the result in a slice called products-slice which has an array as its initial state empty. Until then everything is in order. As I need information the time the homepage is loaded, I use the useEffect hook to fetch the products I have registered. Then I pass this array as props to a component, and make a map. The problem is that when the component loads, the information is not local.
código do backend
module.exports.fetchProduct = async (req, res) => {
try {
const products = await Product.find({});
if (products) {
res.json(products);
}
} catch (error) {
console.log(error);
}
};
productsActions.js
export const fetchProducts = () => {
return async (dispatch) => {
try {
const response = await fetch("http://localhost:xxxx/xxxxxx");
const data = await response.json();
let loadedProducts = [];
for (const key in data) {
loadedProducts.push({
id: data[key]._id,
productName: data[key].productName,
price: data[key].price,
imageUrl: data[key].imageUrl,
});
}
dispatch(setProducts(loadedProducts));
} catch (error) {
console.log(error);
}
};
};
home.jsx
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Container } from "react-bootstrap";
import {fetchProducts} from '../../store/actions/productsActions';
import Hero from "../hero/Hero";
import Footer from "../footer/Footer";
import DisplayProductsList from "../displayProduct/DisplayProductsList";
export default function Home() {
const productsInfo = useSelector((state) => state.products.products);
console.log(productsInfo);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProducts());
}, [dispatch]);
return (
<>
<Hero />
<DisplayProductsList products={productsInfo} />
<Container fluid>
<Footer></Footer>
</Container>
</>
);
}
product-slice.js
const initialState = {
products: [],
};
const productSlice = createSlice({
name: "product",
initialState,
reducers: {
setProducts(state, action) {
state.products.push(action.payload);
},
},
});
export const { setProducts } = productSlice.actions;
export default productSlice.reducer;
component where I'm mapping
export default function DisplayProductsList(props) {
console.log(props);
return (
props.products.map((product) => (
<DisplayProducts
key={product.id}
imageUrl={product.imageUrl}
name={product.productName}
price={product.price}
/>
))
);
}
console.log output in the above component
enter image description here

Related

react/redux fetching data from redux slice issue

I am creating a react/ redux app using json fake api server redux toolkit.
I am adding a some datas from and api to redux slice and trying to retrieve to my component. data seems empty and no error showing could anyone able to help me in this if possible.
my redux slice
import {
createAsyncThunk,
createSlice,
createSelector,
} from "#reduxjs/toolkit";
import axios from "axios";
import { base_emp } from "./api";
const initialState = {
emplist: [],
loading: "loading",
};
export const fetchEmployees = createAsyncThunk(
"employee/emplist",
async (_, thunkAPI) => {
try {
const response = await axios.get(base_emp);
return await response.json();
} catch (error) {
return thunkAPI.rejectWithValue({ error: error.message });
}
}
);
export const userSlice = createSlice({
name: "user",
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
decrement: (state) => {
state.value -= 1;
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
extraReducers: (builder) => {
builder.addCase(fetchEmployees.pending, (state) => {
state.emplist = [];
state.loading = "loading";
});
builder.addCase(fetchEmployees.fulfilled, (state, { payload }) => {
state.emplist = payload;
state.loading = "loaded";
});
builder.addCase(fetchEmployees.rejected, (state, action) => {
state.loading = "error";
state.error = action.error.message;
});
},
});
export const { increment, decrement, incrementByAmount } = userSlice.actions;
export const selectCount = (state) => state.counter.value;
export const selectEmployees = createSelector(
(state) => ({
products: state.emplist,
loading: state.loading,
}),
(state) => state
);
export default userSlice.reducer;
my component goes here
import React, { useState, useEffect } from "react";
import EmployeeDetails from "./EmployeeDetails";
import { useSelector, useDispatch } from "react-redux";
import { fetchEmployees, selectEmployees } from "./features/auth/userSlice";
const Employelist = () => {
const [employe, setEmployee] = useState([]);
const dispatch = useDispatch();
const { emplist } = useSelector(selectEmployees);
React.useEffect(() => {
dispatch(fetchEmployees());
}, [dispatch]);
useEffect(() => {
emplist &&
emplist.then((res) => res.json()).then((data) => setEmployee(data));
}, []);
const handleclick = (id) => {
const emp = employe.filter((emp) => emp.employeeid !== id);
setEmployee(emp);
};
// console.log(currentUsers);
return (
<div>
<EmployeeDetails handleclick={handleclick} employe={employe} />
</div>
);
};
export default Employelist;
api.js
import axios from "axios";
export const base_emp = axios.get("http://localhost:5000/emp");
error message in console

Getting undefined props in functional react components

How to pass the {requests} prop to the RequestRow component after executing the setRequests? My understanding is that the requests get initialized as undefined in the beginning and before being set with the asynchronously called object, it gets passed to the RequestRow component as undefined, and the error occurs.
import React, { useState, useEffect } from 'react';
import 'semantic-ui-css/semantic.min.css';
import Layout from '../../../components/Layout';
import { Button } from 'semantic-ui-react';
import { Link } from '../../../routes';
import Campaign from '../../../blockchain/campaign';
import { Table } from 'semantic-ui-react';
import RequestRow from '../../../components/RequestRow';
const RequestsIndex = ({ address }) => {
const { Header, Row, HeaderCell, Body } = Table;
const campaign = Campaign(address);
const [requestCount, setRequestCount] = useState();
const [requests, setRequests] = useState([]);
const getRequests = async () => {
const count = await campaign.methods.getRequestsCount().call();
setRequestCount(count);
};
let r;
const req = async () => {
r = await Promise.all(
Array(parseInt(requestCount))
.fill()
.map((_element, index) => {
return campaign.methods.requests(index).call();
})
);
setRequests(r);
};
useEffect(() => {
getRequests();
if (requestCount) {
req();
}
}, [requestCount]);
return (
<Layout>
<h3>Requests List.</h3>
<Link route={`/campaigns/${address}/requests/new`}>
<a>
<Button primary>Add Request</Button>
</a>
</Link>
<Table>
<Header>
<Row>
<HeaderCell>ID</HeaderCell>
<HeaderCell>Description</HeaderCell>
<HeaderCell>Amount</HeaderCell>
<HeaderCell>Recipient</HeaderCell>
<HeaderCell>Approval Count</HeaderCell>
<HeaderCell>Approve</HeaderCell>
<HeaderCell>Finalize</HeaderCell>
</Row>
</Header>
<Body>
<Row>
<RequestRow requests={requests}></RequestRow>
</Row>
</Body>
</Table>
</Layout>
);
};
export async function getServerSideProps(context) {
const address = context.query.address;
return {
props: { address },
};
}
export default RequestsIndex;
The RequestRow component is shown below. It takes in the {requests} props, which unfortunately is undefined.
const RequestRow = ({ requests }) => {
return requests.map((request, index) => {
return (
<>
<div>Request!!!</div>
</>
);
});
};
export default RequestRow;
The snapshot of the error is shown below:
I think React is trying to render your component before your promises resolve. If that's the case, all you need to do is set a default value (an empty array in your case) for your requests.
const [requests, setRequests] = useState([]);
May the force be with you.

React test a component with saga

Hllo Guys, I'm having a bit trouble with testing my component
The problem is that I would like to test my React Native Component that uses saga to fetch data from server.
The Problem is that I do know what I'm supposed to do, I think I should mock my API calls in my test file but I do not know how :/
The component file is really simple, when mounted it dispatches action to fetch list on vehicles, and then it shows them in UI. And until that is fetched it shows loading text
Bellow are my current setup of components & test file.
Here is a screen component that fetches initial data on screen load
Screen Component
import React, { useContext, useEffect, useState } from 'react';
import { Platform, FlatList, View, ActivityIndicator, Text } from 'react-native';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { vehiclesActions } from '_store/vehicles';
export const MainScreen = ({ navigation }) => {
/**
* Redux selectors and dispatch
*/
const {
loading = true,
vehicles = [],
loadMore = false
} = useSelector((state) => state.vehicles);
/**
* Initial effect, fetches all vehicles
*/
useEffect(() => {
dispatch(
vehiclesActions.vehicleGet({
page: 1,
})
);
}, []);
const renderCard = () => {
return (<View><Text>Test</Text></View>)
}
if (loading) {
return (<View><Text>App Loading </Text></View>
}
return (
<View style={styles.wrapper}>
<View
style={
Platform.OS === 'ios' ? { marginTop: 30 } : { marginTop: 0, flex: 1 }
}
>
{!loading && (
<View style={Platform.OS === 'ios' ? {} : { flex: 1 }}>
<FlatList
testID={'flat-list'}
data={vehicles}
renderItem={renderCard}
/>
</View>
)}
</View>
</View>
);
};
MainScreen.propTypes = {
navigation: PropTypes.object
};
export default MainScreen;
My Vehicles Saga:
const api = {
vehicles: {
getVehicles: (page) => {
return api.get(`/vehicles/list?page=${page}`, {});
},
}
function* getVehicles(action) {
try {
const { page } = action.payload;
const { data } = yield call(api.vehicles.getVehicles, page);
yield put({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload: data });
} catch (err) {
yield call(errorHandler, err);
yield put({ type: vehiclesConstants.VEHICLE_GET_FAIL });
}
}
export function* vehiclesSaga() {
yield takeLatest(vehiclesConstants.VEHICLE_GET_REQUEST, getVehicles);
}
Actions:
export const vehiclesActions = {
vehicleGet: payload => ({ type: vehiclesConstants.VEHICLE_GET_REQUEST, payload }),
vehicleGetSuccess: payload => ({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload }),
vehicleGetFail: error => ({ type: vehiclesConstants.VEHICLE_GET_FAIL, error }),
}
Reducer
import { vehiclesConstants } from "./constants";
const initialState = {
vehicles: [],
loading: true,
};
export const vehiclesReducer = (state = initialState, action) => {
switch (action.type) {
case vehiclesConstants.VEHICLE_GET_REQUEST:
return {
...state,
loading: true,
};
case vehiclesConstants.VEHICLE_GET_SUCCESS:
return {
...state,
loading: false,
vehicles: action.payload,
};
}
}
My Test File
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
Like I see above I cannot find element with testId flat-list, since component AppScreen it always show loading text, is there any way I could mock that API call and make this to work ?
Jest allows you to mock any module using jest.mock.
You have to write an alternative to axios.get like this
const vehiclesData = [
// ... put default data here
]
const delay = (ms, value) =>
new Promise(res => setTimeout(() => res(value), ms))
const mockAxiosGet = async (path) => {
let result = null
if (path.includes('vehicles/list') {
const query = new URLSearchParams(path.replace(/^[^?]+\?/, ''))
const page = + query.get('page')
const pageSize = 10
const offset = (page - 1)*pageSize
result = vehiclesData.slice(offset, offset + pageSize)
}
return delay(
// simulate 100-500ms latency
Math.floor(100 + Math.random()*400),
{ data: result }
)
}
Then modify the test file as
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import axios from 'axios'
// enable jest mock on 'axios' module
jest.mock('axios')
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
before(() => {
// mock axios implementation
axios.get.mockImplementation(mockAxiosGet)
})
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
For your use case, read more at Mocking Implementations

State is null, after useEffect hooks

I am trying to set product by dispatching a method in a useEffect. However, state still says null.
index.html
import React, { useEffect, Fragment } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchProductsData } from "../../store/products-actions";
import Promotion from "./components/Promotion";
import Products from "./components/Products";
import ToastUi from "../../shared/ui/ToastUi";
import { Container, Row, Col } from "react-bootstrap";
const Home = () => {
const dispatch = useDispatch();
// const products = useSelector((state) => state.products.products);
const products = useSelector((state) => state.products.productsTest);
const cartQuantity = useSelector((state) => state.cart.quantity);
useEffect(() => {
dispatch(fetchProductsData());
}, [dispatch]);
return (
<Fragment>
<ToastUi
status="Sukses"
title="Notifikasi"
message={`(${cartQuantity}) produk baru berhasil di masukkan keranjang`}
/>
<Container fluid="xl">
<Row>
<Col>
<Promotion />
</Col>
</Row>
<Row className="mt-3" md={3}>
<Products products={products} />
</Row>
</Container>
</Fragment>
);
};
export default Home;
Products still says null after a cycle, apparently it needs second cycle to make that state changed. Not sure how can I make it change in one cycle. Do I need to put the useEffect in the parent ?
EDIT
if I add this, it will work
{products !== null && <Products products={products} />}
// {/* <Products products={products} /> */} //
However, is there a better way or maybe some explanation on why this is happening, Thank you.
EDIT
products-slice.js
import { createSlice } from "#reduxjs/toolkit";
import {
products,
excel_products,
product,
filteredProducts,
productsTest,
} from "../datafiles";
const initialProductsState = {
products,
excel_products,
product,
filteredProducts,
productsTest,
};
const productsSlice = createSlice({
name: "products",
initialState: initialProductsState,
reducers: {
viewExcelProducts(state, action) {
state.excel_products = action.payload;
},
uploadExcelProducts(state) {
if (excel_products.length < 0) {
console.log("error");
} else {
const newProducts = state.products.concat(state.excel_products);
state.products = newProducts;
state.excel_products = [];
}
},
selectProduct(state, action) {
const product = state.products.find((item) => item.id === action.payload);
state.product = product;
},
filterProducts(state, action) {
const filteredProducts = state.products.filter(
(item) => item.type === action.payload
);
state.filteredProducts = filteredProducts;
},
setProducts(state, action) {
state.productsTest = action.payload;
},
},
});
export const productsActions = productsSlice.actions;
export default productsSlice;
products-actions.js
import { productsActions } from "./products-slice";
export const fetchProductsData = () => {
return async (dispatch) => {
const fetchData = async () => {
const response = await fetch("http://localhost:5000/products");
if (!response.ok) {
throw new Error("Could not fetch data!");
}
const data = await response.json();
return data;
};
try {
const productsData = await fetchData();
dispatch(productsActions.setProducts(productsData));
} catch (err) {
console.log("Error: " + err);
}
};
};
What do you mean by it needs second cycle to make that state changed?
fetchProductsData is an async function, I assume. That means that you do not receive data immediately, but after some time (depending on network connection speed, payload size etc). So it is OK that your data arrives later.
Usual approach for async data is to keep isLoading in your state. And use it as following:
const isLoading = useSelector((state) => state.products.isLoading);
...
return (
<Fragment>
...
{isLoading && <Spinner />} // Some loading indicator
{!isLoading && <Products products={products} />}
</Fragment>
);
This way you will indicate to user that some data is being fetched. This is a good UX approach.
isLoading should be set somewhere in your fetchProductsData action, like so:
export const fetchProductsData = () => {
return async (dispatch) => {
...
try {
dispatch(productsActions.setIsLoading(true));
const productsData = await fetchData();
dispatch(productsActions.setProducts(productsData));
} catch (err) {
console.log("Error: " + err);
} finally {
dispatch(productsActions.setIsLoading(false));
}
};
};

React renders my Component before it actually has Data

So I'm hard stuck on this Problem... normally I would just do a "ComponentDidMount" but since I'm trying to avoid using classes and only use react hooks I got stuck with the Problem.
My Component renders before it gets any Data from the API, so my .map function won't work as it has not recieve any data.
Shop.js
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { listShops } from "../../Redux/actions/shopActions";
const Shop = () => {
const userShop = useSelector(state => state.shop);
const auth = useSelector(state => state.auth);
const dispatch = useDispatch();
useEffect(() => {
dispatch(listShops(auth));
}, []);
console.log("Look at my userShop",userShop.shop)
return (
<div>
{userShop.map(shop=>(<div>{shop}</div>))}
{console.log("How often do I Render?")}
</div>
);
};
export default Shop;
ShopAction.js
import {GET_SHOPS} from "./types";
export const listShops = userData => async dispatch =>{
const userId = userData.user.id;
await axios.get(`/api/shops/shops/user/${userId}`)
.then(
res => {
const user = res.data;
dispatch({
type: GET_SHOPS,
payload: user.shops
})})
}
shopReducer.js
const initialState = {}
export default function(state = initialState, action) {
switch (action.type) {
case GET_SHOPS:
return {
...state,
shop:action.payload
}
default:
return state;
}
}
if(!userShop){
return <h1>loading<h1>;
}
return (
<div>
{userShop.map(shop=>(<div>{shop}</div>))}
</div>
);
Return an empty array if state.shop is undefined using short-circuit evaluation:
const userShop = useSelector(state => state.shop || []);
return (
<div>
{userShop && userShop.map(shop=>(<div>{shop}</div>))}
</div>
);

Categories

Resources