React Native - Navigating by ID from api with Redux not working - javascript

So, i'am using redux for my application and so far it's good. But recently I just hit a wall that stops me from coding for a while. I'am trying to make a navigation using the ID of every item in my database with redux. let's say I have a "category of food" page that is filled with item from my database and each of those items has their own ID, using those ID I'am trying to navigate to a second page which is the "dishes page". If a user clicks a category, the application should display the dishes that the category contains.
As of now, I manage to display the category but I don't know how to apply the navigation.
Whenever I try to add a kind of navigation that I know I'am facing an error:
Error
state_error ( ** UPDATE ** )
You can see all the details in my code.
Here is my code:
App.js ( ** EDIT ** )
import React, { Component } from 'react';
import {
Platform,
StyleSheet,
Text,
View
} from 'react-native';
import { StackNavigator } from 'react-navigation';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import Reducer from './app/redux/reducers/Reducer';
import AppContainer from './app/container/AppContainer';
import DishContainer from './app/container/DishContainer';
import Dishes from './app/component/Dishes';
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
const store = createStoreWithMiddleware(Reducer);
export default class App extends Component{
render() {
return (
<Provider store = { store }>
<Root />
</Provider>
);
}
}
const Root = StackNavigator ({
AppContainer : { screen: AppContainer },
DishContainer : { screen: DishContainer },
Dishes : { screen: Dishes },
})
Action.js
import {
FETCHING_CATEGORY_REQUEST,
FETCHING_CATEGORY_SUCCESS,
FETCHING_CATEGORY_FAILURE,
FETCHING_DISHES_REQUEST,
FETCHING_DISHES_SUCCESS,
FETCHING_DISHES_FAILURE,
} from "./types";
import axios from 'axios';
/* ---------------------- CATEGORY ---------------------------- */
export const fetchingCategoryRequest = () => ({
type: FETCHING_CATEGORY_REQUEST
});
export const fetchingCategorySuccess = (json) => ({
type: FETCHING_CATEGORY_SUCCESS,
payload: json,
});
export const fetchingCategoryFailure = (error) => ({
type: FETCHING_CATEGORY_FAILURE,
payload: error
});
export const fetchCategory = () => {
return (dispatch) => {
axios.get('http://192.168.254.100:3308/categories/')
.then(response => {
dispatch({ type: FETCHING_CATEGORY_SUCCESS, payload: response.data })
})
.catch(error => console.log(error.response.data));
}
}
/* ---------------------- DISHES ---------------------------- */
export const fetchingDishesRequest = () => ({
type: FETCHING_DISHES_REQUEST
});
export const fetchingDishesSuccess = (json) => ({
type: FETCHING_DISHES_SUCCESS,
dish: json,
});
export const fetchingDishesFailure = (error) => ({
type: FETCHING_DISHES_FAILURE,
dish: error,
});
export const fetchDish = () => {
return (dispatch) => {
const { params } = this.props.navigation.state;
axios.get('http://192.168.254.100:3308/categories/' + params.id)
.then(response => {
dispatch({ type: FETCHING_DISHES_SUCCESS, dish: response.data })
})
.catch(error => console.log(error.response.data));
}
}
Reducer.js
const initialState = {
isFetching: false,
errorMessage: '',
category: [],
dishes: [],
};
const categoryReducer = (state = initialState, action) => {
switch(action.type) {
case FETCHING_CATEGORY_REQUEST:
return {
...state,
isFetching: true
};
case FETCHING_CATEGORY_FAILURE:
return {
...state,
isFetching: false,
errorMessage: action.payload
};
case FETCHING_CATEGORY_SUCCESS:
return {
...state,
isFetching: false,
category: action.payload
};
/* --------------------- DISHES ------------------------- */
case FETCHING_DISHES_REQUEST:
return {
...state,
isFetching: true
};
case FETCHING_DISHES_SUCCESS:
return {
...state,
isFetching: false,
dishes: action.dish
};
case FETCHING_DISHES_FAILURE:
return {
...state,
isFetching: false,
errorMessage: action.dish
}
default:
return state;
}
}
export default categoryReducer;
CategoryList.js
export default class CategoryList extends Component {
_renderItem = ({ item }) => {
const { cat_name } = item;
return (
<View style={styles.cardContainerStyle}>
<View style={{ paddingRight: 5 }}>
<TouchableOpacity style = { styles.buttonContainer }>
<Text style={styles.cardTextStyle}
onPress = { () => this.props.navigation.navigate('Dishes', { id: item.cat_id })}>
{cat_name}
</Text>
</TouchableOpacity>
</View>
</View>
);
};
render() {
return (
<FlatList
style={{ flex: 1 }}
data = {this.props.category}
keyExtractor={(item, index) => index.toString()}
renderItem={this._renderItem}
/>
)
}
}
AppContainer.js
import CategoryList from "../component/CategoryList";
import { fetchCategory } from '../redux/actions/Actions';
import { connect } from 'react-redux';
class AppContainer extends Component {
componentDidMount() {
this.props.fetchCategory();
}
render() {
let content = <CategoryList category = { this.props.randomCategory.category }/>;
if (this.props.randomCategory.isFetching) {
content = <ActivityIndicator size="large"/>;
}
return <View style={styles.container}>{content}</View>;
}
}
const mapStateToProps = state => {
return {
randomCategory: state
};
}
export default connect(mapStateToProps, { fetchCategory })(AppContainer);
Dishes.js (This is where I want to navigate from Category)
export default class Dishes extends Component {
_renderItem = ({ item }) => {
const { cat_desc } = item;
return (
<View style={styles.cardContainerStyle}>
<View style={{ paddingRight: 5 }}>
<TouchableOpacity>
<Text style={styles.cardTextStyle}>
{cat_desc}
</Text>
</TouchableOpacity>
</View>
</View>
);
};
render() {
const { params } = this.props.navigation.state;
return (
<FlatList
style={{ flex: 1 }}
data = {this.props.dishes}
keyExtractor={(item, index) => index.toString()}
renderItem={this._renderItem}
/>
)
}
}
DishContainer.js
import Dishes from "../component/Dishes";
import { fetchDish } from '../redux/actions/Actions';
import { connect } from 'react-redux';
class DishContainer extends Component {
componentDidMount() {
this.props.fetchDish();
}
render() {
let content = <Dishes dishes = { this.props.randomDishes.dishes }/>;
if (this.props.randomDishes.isFetching) {
content = <ActivityIndicator size="large"/>;
}
return <View style={styles.container}>{content}</View>;
}
}
const mapStateToProps = state => {
return {
randomDishes: state
};
}
EDIT: Some how the params is not working or the props.navigation.navigate is not calling the item's id from my api. I'am not sure, please help me.

Edit your AppContainer.js like below :
import CategoryList from "../component/CategoryList";
import { fetchCategory } from '../redux/actions/Actions';
import { connect } from 'react-redux';
class AppContainer extends Component {
componentDidMount() {
this.props.fetchCategory();
}
render() {
let content = <CategoryList
navigation = {this.props.navigation}
category = {this.props.randomCategory.category}/>;
if (this.props.randomCategory.isFetching) {
content = <ActivityIndicator size="large"/>;
}
return <View style={styles.container}>{content}</View>;
}
}
const mapStateToProps = state => {
return {
randomCategory: state
};
}
export default connect(mapStateToProps, { fetchCategory })(AppContainer);
The issue is CategoryList can't get navigation props of parent component(AppCountainer), So you have to pass navigation as props.

Related

How do I render a Next.js page using React Testing Library?

I am writing a unit test for Next.js application.
The components side of the 'pages' folder return 'NextPage', in typescript, where
NextPage is imported from 'next'.
For example: import { NextPage } from 'next'; const BenchmarksPage: NextPage = () => {...
There are other components inside the 'pages' folder.
These components do not return 'NextPage', but return 'FC', which is imported from 'react'.
For example: import { FC } from 'react'; const RegisterJWT: FC = (props) => {...
In my Jest/Testing Library, I render these components like this:
render(<Component />)
expect(...)
Is this the correct way to render both the NextPage component and FC component?
Only FC components are rendering, but not NextPage components.
When rendering NextPage components, it returns <body></div></body>, an empty DOM tree.
EDIT:
Here is a NextPage component, that does not render in test:
import {
Box,
capitalize,
Container,
FormControl,
InputLabel,
MenuItem,
Select,
} from '#material-ui/core';
import { NextPage } from 'next';
import React, { useCallback, useEffect, useState } from 'react';
import { Helmet } from 'react-helmet-async';
import RequireScope from 'src/components/authentication/RequireScope';
import BenchmarkTable from 'src/components/benchmark/BenchmarkTable';
import DashboardLayout from 'src/components/dashboard/DashboardLayout';
import Heading from 'src/components/Heading';
import useSettings from 'src/hooks/useSettings';
import gtm from 'src/lib/gtm';
import { useDispatch, useSelector } from 'src/store';
import { getAllRollingTwelveCalcs } from 'src/store/rolling-twelve/rolling-twelve.thunk';
import { Timeframe, timeframeMap } from 'src/types/benchmark';
const BenchmarksPage: NextPage = () => {
const { settings } = useSettings();
const dispatch = useDispatch();
const [selectedTimeframe, setSelectedTimeframe] = useState<Timeframe>(
Timeframe.Monthly,
);
const company = useSelector((state) => state.company.current);
useEffect(() => {
gtm.push({ event: 'page_view' });
}, []);
useEffect(() => {
dispatch(getAllRollingTwelveCalcs());
}, [company]);
const handleTimeframeChange = useCallback(
(
event: React.ChangeEvent<{
name?: string;
value: Timeframe;
event: Event | React.SyntheticEvent<Element, Event>;
}>,
) => {
setSelectedTimeframe(event.target.value);
},
[],
);
return (
<RequireScope scopes={['query:benchmark-calcs']}>
<DashboardLayout>
<Helmet>
<title>Benchmarks</title>
</Helmet>
<Container maxWidth={settings.compact ? 'xl' : false}>
<Box
sx={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
mb: 4,
}}
>
<Heading>Benchmarks</Heading>
<FormControl sx={{ width: 300 }}>
<InputLabel>Timeframe</InputLabel>
<Select
sx={{ background: '#ffffff', maxWidth: 400 }}
value={selectedTimeframe}
label="Timeframe"
onChange={handleTimeframeChange}
>
{[...timeframeMap.keys()].map((timeframe) => (
<MenuItem key={timeframe} value={timeframe}>
{capitalize(timeframe)}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
<BenchmarkTable
timeframe={selectedTimeframe}
data-testid="benchmark-table"
/>
</Container>
</DashboardLayout>
</RequireScope>
);
};
export default BenchmarksPage;
It uses thunk, which is here:
import { createAsyncThunk } from '#reduxjs/toolkit';
import axios from 'src/lib/axios';
import { RootState } from 'src/store';
import {
RollingTwelveAnalysisCategoryCalculation,
RollingTwelveManualInputLineItemCalculation,
} from 'src/types/rolling-twelve';
export const getAllRollingTwelveCalcs = createAsyncThunk<
{
anaylsisCategoryCalcs: RollingTwelveAnalysisCategoryCalculation[];
manualInputCalcs: RollingTwelveManualInputLineItemCalculation[];
},
undefined,
{
state: RootState;
}
>('rollingTwelve/getAll', async () => {
const response = await axios.get(`/rolling-twelve/all`);
return response.data;
});
Now here is a component, that is 'FC' (not NextPage) that does render. This still is inside pages folder:
import { useAuth0 } from '#auth0/auth0-react';
import {
Box,
Button,
Card,
CardContent,
Container,
Typography,
} from '#material-ui/core';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type { FC } from 'react';
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet-async';
import Logo from 'src/components/Logo';
import axios from 'src/lib/axios';
import gtm from 'src/lib/gtm';
const Login: FC = () => {
const router = useRouter();
const { isLoading, isAuthenticated, loginWithRedirect } = useAuth0();
useEffect(() => {
gtm.push({ event: 'page_view' });
}, []);
useEffect(() => {
if (!isLoading && isAuthenticated) {
router.push('/recommendations');
}
}, [isLoading, isAuthenticated]);
const handleIntuitLogin = async () => {
try {
const response = await axios.get('/auth/sign-in-with-intuit');
window.location = response.data;
} catch (e) {
throw new Error(e);
}
};
const handleAuth0Login = async () => {
try {
const response = await axios.get('/auth/sign-in-with-auth0');
window.location = response.data;
} catch (e) {
throw new Error(e);
}
};
return (
<>
<Helmet>
<title>Login</title>
</Helmet>
<Box
sx={{
backgroundColor: 'background.default',
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
}}
>
<Container maxWidth="sm" sx={{ py: '80px' }}>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
mb: 8,
}}
>
<Link href="/login">
<Box>
<Logo height={100} width={300} />
</Box>
</Link>
</Box>
<Card>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
p: 4,
}}
>
<Box
sx={{
alignItems: 'center',
display: 'flex',
justifyContent: 'space-between',
mb: 3,
}}
>
<div>
<Typography
color="textPrimary"
gutterBottom
variant="h4"
data-testid="login-title"
>
Log in
</Typography>
</div>
</Box>
<Box
sx={{
flexGrow: 1,
mt: 3,
}}
>
<Button
color="primary"
onClick={handleAuth0Login}
fullWidth
size="large"
type="button"
variant="contained"
>
Sign In
</Button>
</Box>
<Box sx={{ mt: 2 }}>
<Button
color="primary"
onClick={handleIntuitLogin}
fullWidth
size="large"
type="button"
variant="contained"
>
Sign In With Intuit
</Button>
</Box>
</CardContent>
</Card>
</Container>
</Box>
</>
);
};
export default Login;
EDIT 2:
RequireScope:
import React, { useEffect, useState } from 'react';
import useAuth from 'src/hooks/useAuth';
export interface RequireScopeProps {
scopes: string[];
}
const RequireScope: React.FC<RequireScopeProps> = React.memo((props) => {
const { children, scopes } = props;
const { isInitialized, isAuthenticated, permissions } = useAuth();
const [isPermitted, setIsPermitted] = useState(false);
useEffect(() => {
if (isAuthenticated && isInitialized) {
(async () => {
const hasPermissions = scopes
.map((s) => {
return permissions.includes(s);
})
.filter(Boolean);
if (hasPermissions.length === scopes.length) {
setIsPermitted(true);
}
})();
}
}, [isAuthenticated, isInitialized, scopes, permissions]);
if (isPermitted) {
return <>{children}</>;
}
return null;
});
export default RequireScope;
Benchmarks.test.tsx
import '#testing-library/jest-dom';
import { render, screen, waitFor, within } from '#testing-library/react';
import { HelmetProvider } from 'react-helmet-async';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import BenchmarksPage from '../src/pages/benchmarks/index';
import { initState } from './mockState'; // this is just an object with nested objects and arrays
test('renders header, timeframe dropdown and the chart image', async () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
render(
<HelmetProvider>
<Provider store={mockStore(initState)}>
<BenchmarksPage />
</Provider>
</HelmetProvider>,
);
// Note: NONE of these 5 queries below work. Component is not rendering in the DOM
const header = screen.getByRole('heading', { name: /benchmarks/i });
const header = screen.getByRole('heading');
const header = await screen.findByRole('heading', { name: /benchmarks/i });
const header = await waitFor(() => screen.getByText(/benchmarks/i));
const header = await waitFor(() => expect(screen.getByText(/benchmarks/i)), {
timeout: 4000,
});
screen.debug(); // doesn't work. <body></div></body>
}
EDIT3:
useAuth in RequireScope
import { useContext } from 'react';
import AuthContext from '../contexts/JWTContext';
const useAuth = () => useContext(AuthContext);
export default useAuth;
JWTContext above:
Just really long, dispatches to /login using JWT token etc.
EDIT 4:
JWTContext component
import { useRouter } from 'next/router';
import PropTypes from 'prop-types';
import {
createContext,
FC,
ReactNode,
useCallback,
useEffect,
useReducer,
} from 'react';
import { useDispatch } from 'src/store';
import { companyActions } from 'src/store/company/company.slice';
import {
getAllCompanies,
getCurrentCompany,
} from 'src/store/company/company.thunk';
import axios from '../lib/axios';
export interface AuthUser {
sub?: string;
nickname?: string;
email?: string;
name?: string;
picture?: string;
locale?: string;
updated_at?: Date;
}
interface State {
isInitialized: boolean;
isAuthenticated: boolean;
permissions: string[];
user: AuthUser;
}
interface AuthContextValue extends State {
platform: 'JWT';
login: () => Promise<void>;
logout: () => Promise<void>;
}
interface AuthProviderProps {
children: ReactNode;
}
type InitializeAction = {
type: 'INITIALIZE';
payload: {
isAuthenticated: boolean;
permissions: string[];
user: AuthUser;
};
};
type LoginAction = {
type: 'LOGIN';
};
type LogoutAction = {
type: 'LOGOUT';
};
type Action = InitializeAction | LoginAction | LogoutAction;
const initialState: State = {
isAuthenticated: false,
isInitialized: false,
permissions: [],
user: undefined,
};
const setSession = (
accessToken?: string,
permissions?: string,
user?: string,
): void => {
if (accessToken && permissions) {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('perms', permissions);
localStorage.setItem('user', user);
//axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
} else {
localStorage.removeItem('accessToken');
localStorage.removeItem('perms');
localStorage.removeItem('user');
//delete axios.defaults.headers.common.Authorization;
}
};
const handlers: Record<string, (state: State, action: Action) => State> = {
INITIALIZE: (state: State, action: InitializeAction): State => {
const { isAuthenticated, permissions, user } = action.payload;
return {
...state,
isAuthenticated,
isInitialized: true,
permissions,
user,
};
},
LOGIN: (state: State): State => {
return {
...state,
isAuthenticated: true,
};
},
LOGOUT: (state: State): State => ({
...state,
isAuthenticated: false,
permissions: [],
}),
};
const reducer = (state: State, action: Action): State =>
handlers[action.type] ? handlers[action.type](state, action) : state;
const AuthContext = createContext<AuthContextValue>({
...initialState,
platform: 'JWT',
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
});
export const AuthProvider: FC<AuthProviderProps> = (props) => {
const { children } = props;
const [state, dispatch] = useReducer(reducer, initialState);
const router = useRouter();
const reduxDispatch = useDispatch();
useEffect(() => {
const initialize = async (): Promise<void> => {
try {
if (router.isReady) {
const { token, permissions, user, companyId } = router.query;
// TODO: Move all of this stuff from query and localstorage into session
const accessToken =
(token as string) || window.localStorage.getItem('accessToken');
const permsStorage = window.localStorage.getItem('perms');
const perms = (permissions as string) || permsStorage;
const userStorage = window.localStorage.getItem('user');
const selectedCompanyId =
(companyId as string) || window.localStorage.getItem('companyId');
const authUser = (user as string) || userStorage;
if (accessToken && perms) {
setSession(accessToken, perms, authUser);
try {
// check if user is admin by this perm, probably want to add a flag later
if (perms.includes('create:calcs')) {
if (!selectedCompanyId) {
const response = await reduxDispatch(getAllCompanies());
const companyId = response.payload[0].id;
reduxDispatch(companyActions.selectCompany(companyId));
reduxDispatch(getCurrentCompany({ companyId }));
} else {
reduxDispatch(
companyActions.selectCompany(selectedCompanyId),
);
await reduxDispatch(
getCurrentCompany({ companyId: selectedCompanyId }),
);
}
} else {
reduxDispatch(companyActions.selectCompany(selectedCompanyId));
await reduxDispatch(
getCurrentCompany({ companyId: selectedCompanyId }),
);
}
} catch (e) {
console.warn(e);
} finally {
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: true,
permissions: JSON.parse(perms),
user: JSON.parse(authUser),
},
});
}
if (token || permissions) {
router.replace(router.pathname, undefined, { shallow: true });
}
} else {
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
permissions: [],
user: undefined,
},
});
setSession(undefined);
if (router.pathname !== '/client-landing') {
router.push('/login');
}
}
}
} catch (err) {
console.error(err);
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
permissions: [],
user: undefined,
},
});
//router.push('/login');
}
};
initialize();
}, [router.isReady]);
const login = useCallback(async (): Promise<void> => {
const response = await axios.get('/auth/sign-in-with-intuit');
window.location = response.data;
}, []);
const logout = useCallback(async (): Promise<void> => {
const token = localStorage.getItem('accessToken');
// only logout if already logged in
if (token) {
dispatch({ type: 'LOGOUT' });
}
setSession(null);
router.push('/login');
}, [dispatch, router]);
return (
<AuthContext.Provider
value={{
...state,
platform: 'JWT',
login,
logout,
}}
>
{state.isInitialized && children}
</AuthContext.Provider>
);
};
AuthProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default AuthContext;
When I render a component by wrapping it with AuthProvider, I get the error
TypeError: Cannot read property 'isReady' of null
217 |
218 | initialize();
> 219 | }, [router.isReady]);
I searched the whole day how to make the router not to be null, but I could not.
If there are any other suggestions on how to simulate a logged in user given the code above, I would greatly appreciate it. THank 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

Unable to Render a FlatList

I'm trying to render a FlatList but I don't see it on the Screen
I get my Data of my Reducer through a Console Log and in the Console I get the correct Data:
User {
"apellido": "Alas",
"correo": "prueba#uno.com",
"id": 2021-02-08T23:52:58.855Z,
"nombre": "Rene",
"telefono": "72457183",
}
Here is my Code:
import React from "react";
import { View, Text, FlatList, StyleSheet } from "react-native";
import { useSelector } from 'react-redux';
import UserItem from "../components/UserItem";
export default function Page2() {
const users = useSelector(state => state.registro.availableUsers);
console.log(users);
if (users.length === 0) {
return <View style={styles.centered} >
<Text>No Existen Usuarios</Text>
</View>
}
return (
<FlatList
data={users}
keyExtractor={item => item.id.toString()}
renderItem={
itemData => <UserItem
nombre={itemData.item.nombre}
apellido={itemData.item.apellido}
telefono={itemData.item.telefono}
coreo={itemData.item.coreo}
onSelect={() => { }}
/>
}
/>
)
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#f9fafd',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
}
});
As you can see the Console Log shows there is data being Received from the Reducer, but there is no data displayed through the FlatList
As you can see in the Picture
I tried to get to the same Result (Render a List) with the MAP function but still getting an Error:
TypeError: undefined is not a function (near '...users.map...')
Although I know there shouldn't be an Undefined as there is that printed to the Console
Any Ideas?
PS. I think the Issue might be with the Action or Reducer:
Action
//Import Model
import { createStore } from 'redux';
import User from '../../models/user';
//Constant Creation
export const REGISTER = 'REGISTER';
export const createUser = (nombre, apellido, telefono, correo) => {
return async (dispatch) => {
console.log('entra a fun');
try{
console.log('antes de crear');
const createdUser = new User(new Date(), nombre, apellido, telefono, correo);
console.log('despues de crear');
console.log(createUser);
dispatch({type: REGISTER, user: createdUser});
}
catch (err) {
throw err;
}
}
};
Reducer
import { REGISTER } from '../actions/registro';
const initialState = {
availableUsers: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case REGISTER:
return {
...state,
availableUsers: action.user,
};
}
return state;
};
Your available users is just getting one user, instead of updating/appending the new one.
export default (state = initialState, action) => {
switch (action.type) {
case REGISTER:
return {
...state,
availableUsers: [...state.availableUsers, action.user],
};
}
return state;
};
By spreading (...) state.availableUsers, you can append the new user to the end of the existing array.

Why is my redux app not caching an async api call in redux-thunk?

Am new to redux.
I am working now on an app that shows a list of soccer leagues in each country.
Firstly, I am fetching a countries list. Afterwards, I am using the country name to loop through all countries to get the soccer leagues. Not every country has a soccer league, so I get some null as response, which I filter out. Then I click on a league and I am redirected to a league page. Now comes the tricky part. When I click "back", I go to my main page, but the whole api call process gets fired again. Why? How to prevent it? how to only use the data, that I fetched ones, and only use it as I need to.
If I would guess, than the mistake is somewhere in the reducer. I try there to cache the fetched api call in an object (data: { ...state.data, ...}), but am not sure, if I do this correctly.
The second place, where I could do a m istake is the useEffect. But of course anything else is also possible.
Please help!
Here is my code:
App.js
I use react-router-dom to move between the conatiners:
import React from 'react';
import {Switch, Route, NavLink, Redirect} from "react-router-dom";
import SignedIn from '../signedIn/signedIn';
import SignedOut from '../signedOut/signedOut';
//Components/Containers
import AllLeagues from '../allLeagues/allLeagues/allLeagues';
import League from "../allLeagues/league/league";
const App = () => {
return (
<div className="App">
<nav>
<NavLink to={"/"}>SEARCH</NavLink>
</nav>
<Switch>
<Route path={"/"} exact component={AllLeagues} />
<Route path={"/allLeagues/:league"} exact component={League} />
<Route path={"/signedin"} exact component={SignedIn} />
<Route path={"/signedout"} exact component={SignedOut} />
<Redirect to={"/"} />
</Switch>
</div>
);
}
export default App;
Here is my Page, where I make the api calls to get the countries and the soccer leagues:
allLeagues.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import {Link} from "react-router-dom";
import _ from "lodash";
import shortid from "shortid";
import { allLeagues } from "../../../actions/leagues/allLeagues/allLeagues";
import { allCountries } from "../../../actions/allCountries/allCountries";
//the api provides 255 country names.
const ALL_COUNTRIES_LENGTH = 254;
const AllLeagues = () => {
const dispatch = useDispatch();
const selectAllCountries = useSelector(state => state.allCountries);
const selectAllLeagues = useSelector(state => state.allLeagues);
useEffect(() => {
dispatch(allCountries());
}, [dispatch]);
useEffect(() => {
if(!_.isEmpty(selectAllCountries.data)) {
selectAllCountries.data.countries.map(el => dispatch(allLeagues(el.name_en)));
}
}, [dispatch, selectAllCountries.data]);
let allCountriesArr = [];
let allLeaguesFiltered = [];
let getAllLeagues = [];
allCountriesArr = (Object.values(selectAllLeagues.data));
console.log(Object.values(selectAllLeagues.data));
if(allCountriesArr.length > ALL_COUNTRIES_LENGTH) {
allLeaguesFiltered = allCountriesArr.flat().filter(el => el !== null);
getAllLeagues = allLeaguesFiltered.flat();
}
let getAllZeroDivisionLeagues = [];
let getAllFirstDivisionLeagues = [];
let getAllSecondDivisionLeagues = [];
let getAllThirdDivisionLeagues = [];
if(!_.isEmpty(getAllLeagues)) {
getAllZeroDivisionLeagues = getAllLeagues.filter(el => el.strDivision === "0");
getAllFirstDivisionLeagues = getAllLeagues.filter(el => el.strDivision === "1");
getAllSecondDivisionLeagues = getAllLeagues.filter(el => el.strDivision === "2");
getAllThirdDivisionLeagues = getAllLeagues.filter(el => el.strDivision === "3");
}
const showData = () => {
if(!_.isEmpty(selectAllLeagues.data)) {
return(
<div>
Most Favorited Leagues:
<br/>
{getAllZeroDivisionLeagues.map(el => {
return (
<div key={shortid.generate()}>
<p>{el.strLeague}</p>
<Link to={`/allLeagues/${el.strLeague}`}>View</Link>
</div>
)}
)}
<br/>
<br/>
First Leagues:
<br/>
{getAllFirstDivisionLeagues.map(el => {
return (
<div key={shortid.generate()}>
<p>{el.strLeague}</p>
<Link to={`/allLeagues/${el.strLeague}`}>View</Link>
</div>
)}
)}
<br/>
<br/>
Second Leagues:
<br/>
{getAllSecondDivisionLeagues.map(el => {
return (
<div key={shortid.generate()}>
<p>{el.strLeague}</p>
<Link to={`/allLeagues/${el.strLeague}`}>View</Link>
</div>
)}
)}
<br/>
<br/>
Third Leagues:
<br/>
{getAllThirdDivisionLeagues.map(el => {
return (
<div key={shortid.generate()}>
<p>{el.strLeague}</p>
<Link to={`/allLeagues/${el.strLeague}`}>View</Link>
</div>
)}
)}
</div>
)
}
if (selectAllLeagues.loading) {
return <p>loading...</p>
}
if (selectAllLeagues.errorMsg !== "") {
return <p>{selectAllLeagues.errorMsg}</p>
}
return <p>Loading...</p>;
}
return (
<div>
<br/>
<br/>
All Leagues:
<br />
<br />
{showData()}
</div>
)
}
export default AllLeagues;
The both action files:
allCountries.js
import { GET_ALL_COUNTRIES_LOADING, GET_ALL_COUNTRIES_SUCCESS, GET_ALL_COUNTRIES_FAIL } from "../index";
import theSportsDB from "../../apis/theSportsDB";
export const allCountries = () => async (dispatch) => {
try {
dispatch ({
type: GET_ALL_COUNTRIES_LOADING
})
const response = await theSportsDB.get("all_countries.php");
dispatch ({
type: GET_ALL_COUNTRIES_SUCCESS,
payload: response.data
})
} catch (e) {
dispatch ({
type: GET_ALL_COUNTRIES_FAIL
})
}
}
and allCountriesReducer:
import {GET_ALL_COUNTRIES_LOADING, GET_ALL_COUNTRIES_SUCCESS, GET_ALL_COUNTRIES_FAIL} from "../../actions/index";
const DefaultState = {
loading: false,
data: [],
errorMsg: ""
};
const AllCountriesReducer = (state = DefaultState, action) => {
switch (action.type){
case GET_ALL_COUNTRIES_LOADING:
return {
...state,
loading: true,
errorMsg: ""
};
case GET_ALL_COUNTRIES_SUCCESS:
return {
...state,
loading: false,
data: {
...state.data,
countries: action.payload.countries
},
errorMsg: ""
};
case GET_ALL_COUNTRIES_FAIL:
return {
...state,
loading: false,
errorMsg: "unable to get all the Countries"
};
default:
return state;
}
}
export default AllCountriesReducer;
Now the files, with which I fetch the all the leagues (with the country name, that I got from allCountries):
import { GET_ALL_LEAGUES_LOADING, GET_ALL_LEAGUES_SUCCESS, GET_ALL_LEAGUES_FAIL } from "../../index";
import theSportsDB from "../../../apis/theSportsDB";
export const allLeagues = (country) => async (dispatch) => {
try {
dispatch ({
type: GET_ALL_LEAGUES_LOADING
})
const response = await theSportsDB.get(`search_all_leagues.php?c=${country}&s=Soccer`);
dispatch ({
type: GET_ALL_LEAGUES_SUCCESS,
payload: response.data,
countryName: country
})
} catch (e) {
dispatch ({
type: GET_ALL_LEAGUES_FAIL
})
}
}
and the reducer,
allLeaguesReducer.js
import {GET_ALL_LEAGUES_LOADING, GET_ALL_LEAGUES_SUCCESS, GET_ALL_LEAGUES_FAIL} from "../../../actions/index";
const DefaultState = {
loading: false,
data: {},
errorMsg: ""
};
const AllLeaguesReducer = (state = DefaultState, action) => {
switch (action.type){
case GET_ALL_LEAGUES_LOADING:
return {
...state,
loading: true,
errorMsg: ""
};
case GET_ALL_LEAGUES_SUCCESS:
return {
...state,
loading: false,
data:{
...state.data,
[action.countryName]: action.payload.countrys
},
errorMsg: ""
};
case GET_ALL_LEAGUES_FAIL:
return {
...state,
loading: false,
errorMsg: "unable to get all the leagues"
};
default:
return state;
}
}
export default AllLeaguesReducer;
Also the leagues page itself:
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import {Link} from "react-router-dom";
import _ from "lodash";
import shortid from "shortid";
import { getLeague } from "../../../actions/leagues/league/getLeague";
const League = (props) => {
const leagueName = props.match.params.league;
const dispatch = useDispatch();
const selectLeague = useSelector(state => state.league);
useEffect (() => {
dispatch(getLeague(leagueName));
}, [dispatch, leagueName]);
const showLeague = () => {
if(!_.isEmpty(selectLeague.data)) {
return selectLeague.data.teams.map(el => {
return (
<div key={shortid.generate()}>
{el.strTeam}
</div>
)
})
}
if(selectLeague.loading) {
return <p>loading...</p>
}
if(selectLeague.errorMsg !== "") {
return <p>{selectLeague.errorMsg}</p>
}
return <p>Unable to get the league data</p>
}
return (
<div>
<p>{leagueName}</p>
{showLeague()}
<Link to={"/"}>Back</Link>
</div>
)
}
export default League;
its action file:
import { GET_LEAGUE_LOADING, GET_LEAGUE_SUCCESS, GET_LEAGUE_FAIL } from "../../index";
import theSportsDB from "../../../apis/theSportsDB";
export const getLeague = (league) => async (dispatch) => {
try {
dispatch ({
type: GET_LEAGUE_LOADING
})
const response = await theSportsDB.get(`search_all_teams.php?l=${league}`);
dispatch ({
type: GET_LEAGUE_SUCCESS,
payload: response.data,
// leagueName: league
})
} catch (e) {
dispatch ({
type: GET_LEAGUE_FAIL
})
}
}
and the reducer:
import { GET_LEAGUE_LOADING, GET_LEAGUE_SUCCESS, GET_LEAGUE_FAIL } from "../../../actions/index";
const DefaultState = {
loading: false,
data: {},
errorMsg: ""
};
const LeagueReducer = (state = DefaultState, action) => {
switch (action.type) {
case GET_LEAGUE_LOADING:
return {
...state,
loading: true,
errorMsg: ""
};
case GET_LEAGUE_SUCCESS:
return {
...state,
loading: false,
data: action.payload,
errorMsg: ""
};
case GET_LEAGUE_FAIL:
return {
...state,
loading: false,
errorMsg: "league not found"
};
default:
return state
}
}
export default LeagueReducer;
In Redux dev Tools, when I press on back, to get again to my home page, the following is triggered (in status bar):
GET_ALL_COUNTRIES_LOADING
and after some time:
GET_ALL_LEAGUES_SUCCESS
again. So it is making an api call again.
You need to use a conditional in useEffect so that it doesn't run each time you load the page.
Try this:
useEffect(() => {
if (selectAllCountries.data.length < 1) {
disptch(getCountries());
}
})

How can I filter data using Redux but keeping the old state?

I am working on a search functionality with Redux but I am having some issues.
These are the actions related to the search stuff:
export const passengersDataAction = passengersData => ({
type: ActionTypes.PASSENGERS_DATA,
// This is the array of objects that I need to search through
payload: { passengersData },
});
export const searchParamAction = searchParam => ({
type: ActionTypes.SEARCH_PARAM,
// This is the param that I need to send to passengersData
// in order to get a new array of objects based on the searchParam
payload: { searchParam },
});
Reducers:
const initialState = {
passengersData: [],
searchParam: '',
};
const handlers = {
[ActionTypes.PASSENGERS_DATA](state, action) {
return {
...state,
passengersData: action.payload.passengersData,
};
},
[ActionTypes.SEARCH_PARAM](state, action) {
return {
...state,
searchParam: action.payload.searchParam,
};
},
};
Btw this is how the array of objects looks:
[
{
"id": 3,
"name": "Marcos Alonso",
"address": "Sabana",
"phone": "712321222",
"pickup": 0,
"cardinalpoint": "N",
"latitude": "9.93683450",
"longitude": "-84.10991830",
"timestamp": "2019-02-19 21:23:46",
"dropofftimestamp": null,
"pickuptimestamp": null,
"deleted": null,
"driver": 1
},
...
]
This is something I am trying to get it to work:
[ActionTypes.SEARCH_PARAM](state, action) {
//In filter you can add your own logic to get the data
const searchedData = state.passengersData.filter((passenger) => passenger.name === action.payload.searchParam);
return {
...state,
passengersData: searchedData,
searchParam: action.payload.searchParam,
};
},
But with the code above, it is replacing the passesngerData with 'searchedData'. I need to keep the original passengerData so I guess I can create a new state in redux store and return it from the reducer. My question is, how can I do that? Every time I type something in the input, the whole passengersData array goes away and the searched is not returning anything.
What am I missing?
EDIT
I am going to add the code regarding the components that handle the search functionality:
// imports
import { searchParamAction } from '../../screens/HomeScreen/actions/homeScreen';
class AllPassengersList extends Component {
render() {
const {
searchParamActionHandler,
searchParam,
} = this.props;
return (
<View>
<View>
<TextInput
onChangeText={text => searchParamActionHandler(text)}
value={searchParam}
placeholder="Search..."
/>
</View>
<Text>{searchParam}</Text>
<PassengerCardBasedOnRoute searchParam={searchParam} />
</View>
);
}
}
AllPassengersList.propTypes = {
passengersData: PropTypes.oneOfType([PropTypes.array]).isRequired,
searchParam: PropTypes.oneOfType([PropTypes.string]).isRequired,
searchParamActionHandler: PropTypes.oneOfType([PropTypes.func]).isRequired,
};
export default compose(
connect(
store => ({
navigationStore: store.homeScreen.navigation,
searchParam: store.homeScreen.searchParam,
passengersData: store.homeScreen.passengersData,
}),
dispatch => ({
searchParamActionHandler: value => {
dispatch(searchParamAction(value));
},
}),
),
)(AllPassengersList);
The component above is the one holding the search text input.
The one below is the one where I render the array of objects that I need to filter:
import { View } from 'react-native';
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import PassengersInfo from './PassengerInfo';
import { popupsModalsAction } from '../PopupsModals/actions/popupsModals';
const PassengerCardBasedOnRoute = ({
navigationStore,
passengersData,
popupsModalsActionHandler,
searchParam,
}) => {
return (
<View>
{passengersData.map(info => (
<PassengersInfo
key={info.id}
id={info.id}
searchParam={searchParam}
cardinalpoint={info.cardinalpoint}
name={info.name}
address={info.address}
datetime={info.timestamp}
/>
))}
</View>
);
};
PassengerCardBasedOnRoute.propTypes = {
passengersData: PropTypes.oneOfType([PropTypes.array]).isRequired,
searchParam: PropTypes.oneOfType([PropTypes.string]).isRequired,
};
export default compose(
connect(
store => ({
passengersData: store.homeScreen.passengersData,
searchParam: store.homeScreen.searchParam,
}),
),
)(PassengerCardBasedOnRoute);
So passengersData is the array which handles the data I need.
You should not filter the data in redux store and assign the result to the variable you filtered data from because this way on every search you wold loose you original data, instead just store the searchParam in store and write a selector that returns you the filtered result and use that in the component
const filterSelector = (state, props) => {
return state.passengersData.filter((passenger) => passenger.name === state.searchParam);
}
const mapStateToProps = (state, props) => {
const searchData = filterSelector(state, props);
return {
searchData
}
}
and you reducer would simply be
[ActionTypes.SEARCH_PARAM](state, action) {
return {
...state,
searchParam: action.payload.searchParam,
};
}
EDIT: Updating code with example
// imports
import { searchParamAction } from '../../screens/HomeScreen/actions/homeScreen';
class AllPassengersList extends Component {
render() {
const {
searchParamActionHandler,
searchParam,
} = this.props;
return (
<View>
<View>
<TextInput
onChangeText={text => searchParamActionHandler(text)}
value={searchParam}
placeholder="Search..."
/>
</View>
<Text>{searchParam}</Text>
<PassengerCardBasedOnRoute searchParam={searchParam} />
</View>
);
}
}
AllPassengersList.propTypes = {
passengersData: PropTypes.oneOfType([PropTypes.array]).isRequired,
searchParam: PropTypes.oneOfType([PropTypes.string]).isRequired,
searchParamActionHandler: PropTypes.oneOfType([PropTypes.func]).isRequired,
};
const filterSelector = (passengersData, searchParam) => {
return passengersData.filter((passenger) => searchParams == '' || passenger.name === searchParam);
}
const mapStateToProps = store => ({
navigationStore: store.homeScreen.navigation,
searchParam: store.homeScreen.searchParam,
passengersData: filterSelector(state.homeScreen.passengersData, state.homeScreen.searchParam),
}),
export default compose(
connect(
dispatch => ({
searchParamActionHandler: value => {
dispatch(searchParamAction(value));
},
}),
),
)(AllPassengersList);
,

Categories

Resources