My react native application is showing a blank screen after running it
with redux for state management.
The HomeScreen functional component could also be failing to render the Flatlist properly.
Maybe I am using the useSelector hook wrongly or the Redux actions aren't correct. The navigation is handled by the react navigation.
import {
GET_CURRENCY,
GET_CURRENCY_SUCCESS,
GET_CURRENCY_FAILURE
} from "../ActionTypes";
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", "{BearerToken} ");
const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
export const getCurrency = () =>{
return{
type: GET_CURRENCY,
}
}
export const currencySuccess = instruments => {
return{
type: GET_CURRENCY_SUCCESS,
payload: instruments
}}
export const currencyFailure = error => {
return{
type: GET_CURRENCY_FAILURE,
payload:
error
}
}
export const fetchCurrency =() => {
return dispatch => {
dispatch(getCurrency())
fetch("https://api-fxpractice.oanda.com/v3/instruments/EUR_USD/candles?price=M", requestOptions)
// eslint-disable-next-line no-unused-vars
.then(response => response.data)
.then(instruments =>{
dispatch (currencySuccess(instruments))
})
// eslint-disable-next-line no-undef
.catch (error => {
const errorMsg = error.message
dispatch(currencyFailure(errorMsg))
})
}
}
export default fetchCurrency
Homescreen
import React, {useEffect} from 'react'
import { FlatList, ActivityIndicator, View, Text, SafeAreaView} from 'react-native'
import Instrument from '../../../components/Instrument'
import styles from './styles'
import {useSelector, useDispatch} from 'react-redux'
import fetchCurrency from '../../Redux/Actions/currencyActions'
function HomeScreen () {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchCurrency())
}, []);
const { instruments, loading, error} = useSelector(state => state.reducer
);
{error &&
<View><Text>Error</Text></View>
}
{loading && <ActivityIndicator size='large' />}
return(
<SafeAreaView
style={styles.container}
>
<FlatList
data={instruments}
numColumns={1}
contentContainerStyle = {styles.list}
keyExtractor = {({item}) => item.toString() }
renderItem = {(item, index) => (
<Instrument currency={item} />
)}
/>
</SafeAreaView>
);
}
export default HomeScreen
reducer
import {
GET_CURRENCY,
GET_CURRENCY_SUCCESS,
GET_CURRENCY_FAILURE
} from '../ActionTypes'
const initialState = {
instruments: [],
loading: false,
error: null
}
const reducer = (state= initialState, action) => {
switch(action.type){
case GET_CURRENCY:
return {...state, loading: true}
case GET_CURRENCY_SUCCESS:
return {...state, loading: false, instruments: action.payload.instruments }
case GET_CURRENCY_FAILURE:
return { ...state, loading: false, error: action.payload}
default:
return state
}
}
export default reducer;
You need to return the components that you create in the error or loading case.
if (error)
return (<View><Text>Error</Text></View>)
else if (loading)
return (<ActivityIndicator size='large' />)
else
return (<SafeAreaView
...
The coding pattern
{error &&
<View><Text>Error</Text></View>
}
That you have in your code can only be used inside a JSX component to render another component conditionally. For example
<OuterComponent>
{flag && (<OptionalComponent/>)}
</OuterComponent>
It works because the curly braces in JSX contain regular JavaScript code, and the result of flag && (<OptionalComponent/>) is either false or <OptionalComponent> and for React false simply doesn't generate any output.
Related
I'm trying to get initial data from a reducer by dispatching action from App.js component, it works fine but when I switch to another component and load it with useSelector it gets undefined.
I have tried this line of code in Headphones.js but the second one returns undefined
const allData = useSelector((state) => state.allDataReducer);
const { loading, error, data } = allData;
App.js
const dispatch = useDispatch();
useEffect(() => {
dispatch(welcomeAction());
dispatch(listAllData);
}, [dispatch]);
allDataReducer.js
import {
LIST_ALL_DATA_FAIL,
LIST_ALL_DATA_REQUEST,
LIST_ALL_DATA_SUCCESS,
} from "../constants/shared";
export const allDataReducer = (state = { loading: true, data: {} }, action) => {
switch (action.type) {
case LIST_ALL_DATA_REQUEST:
return { loading: true };
case LIST_ALL_DATA_SUCCESS:
return { loading: false, data: action.payload };
case LIST_ALL_DATA_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
shared.js
import {
LIST_ALL_DATA_FAIL,
LIST_ALL_DATA_REQUEST,
LIST_ALL_DATA_SUCCESS,
} from "../constants/shared";
import Axios from "axios";
export const listAllData = async (dispatch) => {
dispatch({
type: LIST_ALL_DATA_REQUEST,
});
try {
const { data } = await Axios.get("/all");
dispatch({ type: LIST_ALL_DATA_SUCCESS, payload: data });
} catch (error) {
dispatch({ type: LIST_ALL_DATA_FAIL, payload: error.message });
}
};
Headphones.js
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { listheadphones } from "../actions/headphonesActions";
import BasicSection from "../components/BasicSection";
import Definer from "../components/Definer";
import LoadingBox from "../components/LoadingBox";
import MessageBox from "../components/MessageBox";
import ProductsCategories from "../components/ProductsCategories";
import BestAudioGear from "../components/BestAudioGear";
const Headphones = (props) => {
const dispatch = useDispatch();
const headphonesList = useSelector((state) => state.headphonesList);
const allData = useSelector((state) => state.allData);
const { loading, error, data } = allData; //undefined
//const { loading, error, headphones } = headphonesList;
console.log(headphonesList);
useEffect(() => {
dispatch(listheadphones());
}, [dispatch]);
return (
<div>
<Definer title="HEADPHONES" />
{loading ? (
<LoadingBox></LoadingBox>
) : error ? (
<MessageBox variant="danger">{error}</MessageBox>
) : (
headphones.map((headphone) => (
<BasicSection
key={headphone.id}
name={headphone.headerName}
info={headphone.info}
mobile={headphone.mobile}
tablet={headphone.tablet}
desktop={headphone.desktop}
/>
))
)}
<ProductsCategories />
<BestAudioGear />
</div>
);
};
export default Headphones;
Github repo
Your description is still not specific enough, can't really pin down what the issue is. But here is some stuff I noticed:
dispatch(listAllData); somehow looks wrong to me, the action creator is usually a function that gets called: dispatch(listAllData());
Then where you define export const listAllData = async (dispatch) => { - this should be a function that returns a function if you're using the thunk middleware. You only defined a function.
I setup a basic auth system following the react-navigation auth flow guide with FeathersJS react-native client.
Here is my main index.tsx file:
import 'react-native-gesture-handler';
import React, {
useReducer, useEffect, useMemo, ReactElement,
} from 'react';
import { Platform, StatusBar } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { AppLoading } from 'expo';
import { useFonts } from '#use-expo/font';
import SplashScreen from './screens/SplashScreen';
import { AuthContext } from './contexts';
import { client } from './utils';
import DrawerNavigator from './navigation/DrawerNavigator';
import LogonStackNavigator from './navigation/LogonStackNavigator';
interface State {
isLoading: boolean;
isSignOut: boolean;
userToken: string|null;
}
const App = (): ReactElement => {
/* eslint-disable global-require */
const [fontsLoaded] = useFonts({
'Lato-Regular': require('../assets/fonts/Lato-Regular.ttf'),
'Lato-Bold': require('../assets/fonts/Lato-Bold.ttf'),
'Poppins-Light': require('../assets/fonts/Poppins-Light.ttf'),
'Poppins-Bold': require('../assets/fonts/Poppins-Bold.ttf'),
});
/* eslint-enable global-require */
const [state, dispatch] = useReducer(
(prevState: State, action): State => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignOut: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignOut: true,
userToken: null,
};
default:
return prevState;
}
},
{
isLoading: true,
isSignOut: false,
userToken: null,
},
);
useEffect(() => {
const bootstrapAsync = async (): Promise<void> => {
let userToken;
try {
const auth = await client.reAuthenticate();
console.log('reAuthenticate:', auth);
userToken = auth.accessToken;
} catch (e) {
// eslint-disable-next-line no-console
console.log('reAuthenticate failure:', e);
}
dispatch({
type: 'RESTORE_TOKEN',
token: userToken,
});
};
bootstrapAsync();
}, []);
const authContext = useMemo(
() => ({
signIn: async (data) => {
// In a production app, we need to send some data (usually username, password) to server and get a token
// We will also need to handle errors if sign in failed
// After getting token, we need to persist the token using `AsyncStorage`
// In the example, we'll use a dummy token
// eslint-disable-next-line no-console
console.log('signIn', data);
try {
const auth = await client.authenticate({
strategy: 'local',
...data,
});
console.log(auth);
dispatch({
type: 'SIGN_IN',
token: 'dummy-auth-token',
});
} catch (e) {
console.log('signIn failure:', e);
}
},
signOut: async () => {
try {
await client.logout();
dispatch({ type: 'SIGN_OUT' });
} catch (e) {
console.log('signOut failure:', e);
}
},
signUp: async (data) => {
// In a production app, we need to send user data to server and get a token
// We will also need to handle errors if sign up failed
// After getting token, we need to persist the token using `AsyncStorage`
// In the example, we'll use a dummy token
// eslint-disable-next-line no-console
console.log('signUp', data);
dispatch({
type: 'SIGN_IN',
token: 'dummy-auth-token',
});
},
}),
[],
);
if (!fontsLoaded) {
return <AppLoading />;
}
if (state.isLoading) {
return <SplashScreen />;
}
return (
<AuthContext.Provider value={authContext}>
{Platform.OS === 'ios' && (
<StatusBar barStyle="dark-content" />
)}
{state.userToken == null ? (
<NavigationContainer>
<LogonStackNavigator />
</NavigationContainer>
) : (
<NavigationContainer>
<DrawerNavigator />
</NavigationContainer>
)}
</AuthContext.Provider>
);
};
export default App;
And my SiginScreen.tsx file which handle the login form:
import React, { ReactElement } from 'react';
import { StyleSheet, KeyboardAvoidingView } from 'react-native';
import { StackNavigationProp } from '#react-navigation/stack';
import { RouteProp } from '#react-navigation/core';
import { AuthContext } from '../contexts';
import {
LogonHeader, Button, Input, Text, TextLink,
} from '../components';
import { LogonStackParamList } from '../navigation/LogonStackNavigator';
interface Props {
navigation: StackNavigationProp<LogonStackParamList, 'SignIn'>;
route: RouteProp<LogonStackParamList, 'SignIn'>;
}
const SignInScreen = ({ navigation }: Props): ReactElement => {
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const { signIn } = React.useContext(AuthContext);
return (
<KeyboardAvoidingView
style={styles.container}
behavior="padding"
>
<LogonHeader title="Se connecter" />
<Input
placeholder="E-mail"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
/>
<Input
placeholder="Mot de passe"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button
title="Connexion"
onPress={() => signIn({
email,
password,
})}
/>
</KeyboardAvoidingView>
);
};
export default SignInScreen;
It works as expected, but I can't figure out how to handle the error case.
Currently, it's just a console.log statement on index.tsx file.
How can I properly informs the SignInScreen component that the logins fail to show a message at the user end? Should I use redux or something?
More exactly: I would like to put an error text message directly on SignInScreen in case of failure.
As I can see, you could add a new property to your global state which could be possibly null that indicates the given Auth error, then you can dispatch everytime the service returns an error. If you are willing to change your approach, I'd suggest that you only store in a global state the user token, you can manage the isLoading individually in each screen and the isSignOut could be derived from the token. You will reduce the the number of re renders and will simplify the logic behind it.
EDIT:
This is a code sample of what you can do:
// main index.tsx
import 'react-native-gesture-handler';
import React, {
useReducer, useEffect, useMemo, ReactElement,
} from 'react';
import { Platform, StatusBar } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { AppLoading } from 'expo';
import { useFonts } from '#use-expo/font';
import SplashScreen from './screens/SplashScreen';
import { AuthContext } from './contexts';
import { client } from './utils';
import DrawerNavigator from './navigation/DrawerNavigator';
import LogonStackNavigator from './navigation/LogonStackNavigator';
interface State {
isLoading: boolean;
isSignOut: boolean;
userToken: string|null;
//we add a new property to your state object to make visible the errors
authError: string|null;
}
const App = (): ReactElement => {
// keep the same code, deleted for simplicity
const [state, dispatch] = useReducer(
(prevState: State, action): State => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
authError: null,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignOut: false,
authError: null,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignOut: true,
authError: null,
userToken: null,
};
case: 'AUTH_ERROR':
return {
...prevState,
authError: action.error
};
default:
return prevState;
}
},
{
isLoading: true,
isSignOut: false,
userToken: null,
authError: null,
},
);
useEffect(() => {
const bootstrapAsync = async (): Promise<void> => {
try {
const auth = await client.reAuthenticate();
console.log('reAuthenticate:', auth);
dispatch({
type: 'RESTORE_TOKEN',
token: auth.accessToken,
});
} catch (e) {
// eslint-disable-next-line no-console
console.log('reAuthenticate failure:', e);
dispatch({
type: 'AUTH_ERROR',
token: e, //or what ever your want to show
});
}
};
bootstrapAsync();
}, []);
const authContext = useMemo(
() => ({
//we add state here in order to access it from
//React.useContext(AuthContext);
state,
//put your code here
}),
[],
);
/*
...YOUR CODE...
*/
Basically, in the code above we added a new property authError to your global state. Then, you need a new action to update that property. Then, in your authContext we add the global state to be retrieved from anywhere.
To keep simplicity, I just added one dispatch with AUTH_ERROR action in your code, but you can do the same in signIn, signUp and signOut.
in your SiginScreen.tsx you can do the following:
const SignInScreen = ({ navigation }: Props): ReactElement => {
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
//we get the state we added in our AuthContext
const { signIn, state } = React.useContext(AuthContext);
return (
<KeyboardAvoidingView
style={styles.container}
behavior="padding"
>
<LogonHeader title="Se connecter" />
<Input
placeholder="E-mail"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
/>
<Input
placeholder="Mot de passe"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button
title="Connexion"
onPress={() => signIn({
email,
password,
})}
/>
{/* shows the error if any, otherwise it won't be shown */}
{state.authError && <Text>{state.authError}</Text>}
</KeyboardAvoidingView>
);
};
I am trying to create a higher order component that displays a spinner when data is fetching from a server and the component with the data when it's done. However with the implementation I have done below it goes straight to rendering the component and not the spinner giving an error the it can not read property length of null.
I think the problem might be related to the initial state I am giving to the reducer with the isFetching: false property. When debugging the application the HOC withSpinner gets an isLoading prop as false. To test withSpinner HOC I set the isFetching initial state as true. This way it only displays a spinner and doesn't continue updating the component. The selected value isFetching from the reducer never updates to false.
Redux actions file
import ProductActionTypes from "./product.types";
import { DB } from "../../api/DB";
const fetchProductsStart = () => ({
type: ProductActionTypes.FETCH_PRODUCTS_START
});
const fetchProductsSuccess = products => ({
type: ProductActionTypes.FETCH_PRODUCTS_SUCCESS,
payload: products
});
const fetchProductsFailure = errorMessage => ({
type: ProductActionTypes.FETCH_PRODUCTS_FAILURE,
payload: errorMessage
});
export const fetchProductsStartAsync = (series, queryStrings) => {
return dispatch => {
dispatch(fetchProductsStart());
DB.Product.getFilteredProducts(series, queryStrings)
.then(products => dispatch(fetchProductsSuccess(products)))
.catch(err => dispatch(fetchProductsFailure(err)));
};
};
Redux reducer
import ProductActionTypes from "./product.types";
const INITIAL_STATE = {
products: null,
errorMessage: null,
isFetching: false,
totalResultsFromQuery: 0
};
const productReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ProductActionTypes.FETCH_PRODUCTS_START:
return {
...state,
isFetching: true
};
case ProductActionTypes.FETCH_PRODUCTS_SUCCESS:
return {
...state,
isFetching: false,
products: action.payload[0],
totalResultsFromQuery: action.payload[1][0].totalResultsFromQuery
};
case ProductActionTypes.FETCH_PRODUCTS_FAILURE:
return {
...state,
isFetching: false,
errorMessage: action.payload
};
default:
return state;
}
};
export default productReducer;
I am using reselect to get the isFetching value from my redux store:
export const selectIsFetching = createSelector(
[selectProducts],
products => products.isFetching
);
The React Component:
import React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import { compose } from "redux";
import { fetchProductsStartAsync } from "../../redux/product/product.actions";
import {
selectProductItems,
selectIsFetching,
} from "../../redux/product/product.selectors";
import {
selectCurrentPage,
selectCapacity
} from "../../redux/filter/filter.selectors";
import withSpinner from "components/withSpinner/withSpinner";
import ProductItemCard from "components/ProductItemCard/ProductItemCard";
import "./ProductsWrapper.css";
import {
selectSearchInput,
selectColor,
selectFamily,
selectPriceFrom,
selectPriceTo
} from "redux/filter/filter.selectors";
import { resetDefault } from "../../redux/filter/filter.actions";
class ProductsWrapper extends React.Component {
componentDidMount() {
const {
fetchProducts,
match: { params },
color,
searchInput,
priceFrom,
priceTo,
family,
page,
capacity
} = this.props;
fetchProducts(params.seria, {
color,
searchInput,
priceFrom,
priceTo,
family,
page,
capacity
});
}
componentDidUpdate(prevProps) {
const {
fetchProducts,
match: { params },
color,
searchInput,
priceFrom,
priceTo,
family,
resetFilterToDefault,
page,
capacity
} = this.props;
if (params.seria !== prevProps.match.params.seria) {
resetFilterToDefault();
fetchProducts(params.seria, {
color,
searchInput,
priceFrom,
priceTo,
family,
page,
capacity
});
}
if (
color !== prevProps.color ||
searchInput !== prevProps.searchInput ||
priceFrom !== prevProps.priceFrom ||
priceTo !== prevProps.priceTo ||
family !== prevProps.family ||
page !== prevProps.page ||
capacity !== prevProps.capacity
) {
fetchProducts(params.seria, {
color,
searchInput,
priceFrom,
priceTo,
family,
page,
capacity
});
}
}
render() {
const { products } = this.props;
return (
<div className="products-outer-wrapper">
<div className="products-wrapper">
{products.length !== 0 ? (
products.map(({ ...productProps }, index) => (
<ProductItemCard {...productProps} key={index} />
))
) : (
<p>No products!</p>
)}
</div>
</div>
);
}
}
const mapStateToProps = state => ({
products: selectProductItems(state),
isLoading: selectIsFetching(state),
color: selectColor(state),
searchInput: selectSearchInput(state),
family: selectFamily(state),
priceFrom: selectPriceFrom(state),
priceTo: selectPriceTo(state),
page: selectCurrentPage(state),
capacity: selectCapacity(state)
});
const mapDispatchToProps = dispatch => ({
fetchProducts: (series, queryStrings) =>
dispatch(fetchProductsStartAsync(series, queryStrings)),
resetFilterToDefault: () => dispatch(resetDefault())
});
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
),
withRouter,
withSpinner
)(ProductsWrapper);
And the higher order component withSpinner:
import React from "react";
import Spinner from "../Spinner/Spinner";
const withSpinner = WrappedComponent => {
const enhancedComponent = ({ isLoading, ...otherProps }) => {
return isLoading ? <Spinner /> : <WrappedComponent {...otherProps} />;
};
return enhancedComponent;
};
export default withSpinner;
As I have noticed, you assign products = null in your default state, which is not a very good practice (imho). Why not assign products = [] to get rid of the error in the first place? Also might be useful to use PropTypes to make sure products is always of correct type...
I want to make a search bar for searching items from the list using react native with redux
I have tried this :
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Text, View, FlatList, Image, ScrollView, StyleSheet, TouchableOpacity, TextInput} from 'react-native';
import { SearchBox, Spinner } from './common';
import { listShow, searchResult} from './actions';
class flatRedux extends Component {
componentWillMount() {
this.props.listShow();
}
_onSearchChange = text => {
this.props.searchResult(text)
this.props.listShow(text)
}
render() {
console.log(this.props);
return (
<View style={styles.MainContainer}>
<SearchBox
placeholder="Search..."
onChangeText={this._onSearchChange}
/>
<FlatList
data={this.props.flatlist}
ItemSeparatorComponent = {this.FlatListItemSeparator}
keyExtractor={(item, index) => index.toString()}
renderItem={({item}) =>
<Text key={item.id} style={styles.FlatListItemStyle} >
{item.cl_name} </Text>}
/>
</View>
);
}
}
const mapStateToProps = state => {
return {
search: state.searchResult.search,
flatlist: state.listShow.flatlist
};
};
export default connect(mapStateToProps, { listShow, searchResult })(flatRedux);
This is the SearchAction file
import { SEARCH_DATA } from './types'
export const searchResult = (text) => {
return {
type: SEARCH_DATA,
payload: text
};
}
And this one is SearchReducer File
import {SEARCH_DATA} from "../actions";
const INITIAL_STATE = {
search: ''
}
export default (state = INITIAL_STATE, action) => {
console.log(action);
switch(action.type) {
case SEARCH_DATA:
return {
...state,
search: action.payload
}
default:
return state;
}
}
And files who are fetching items from the localhost server are below :
flatAction.js
import axios from 'axios';
import { FLAT_DATA } from './types';
export const listShow = () => {
return (dispatch) => {
axios.post('http://192.168.48.228/reactTest/list.php')
.then((response) => {
dispatch({
type: FLAT_DATA,
payload: response.data
});
})
.catch((error) => {
console.log(error);
});
};
};
flatReducer.js
import {FLAT_DATA } from '../actions/types';
const INITIAL_STATE = {
flatlist: '',
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case FLAT_DATA:
return { ...state, flatlist: action.payload };
default:
return state;
}
};
All items are fetching but 'Search' is not working for them.
'Search' is not working for them?
it's not detailed enough to give a specific answer.
To debug the workflow try changing the endpoint of listShow to search and then calling this.props.listShow(text) on input onChange handler the see if the redux part is properly connected
assuming,
All items are fetching
If it is not working need to see if your keyListners are working or not (need to see the code for searchbox), and finally, if the flatlist is rerendered (It doesn't rerender if shallow comparison fails)
I am fairly new to React and Redux and I have an issue with my component not updating on the final dispatch that updates a redux store. I am using a thunk to preload some data to drive various pieces of my site. I can see the thunk working and the state updating seemingly correctly but when the data fetch success dispatch happens, the component is not seeing a change in state and subsequently not re rendering. the interesting part is that the first dispatch which sets a loading flag is being seen by the component and it is reacting correctly. Here is my code:
actions
import { programsConstants } from '../constants';
import axios from 'axios'
export const programsActions = {
begin,
success,
error,
};
export const loadPrograms = () => dispatch => {
dispatch(programsActions.begin());
axios
.get('/programs/data')
.then((res) => {
dispatch(programsActions.success(res.data.results));
})
.catch((err) => {
dispatch(programsActions.error(err.message));
});
};
function begin() {
return {type:programsConstants.BEGIN};
}
function success(data) {
return {type:programsConstants.SUCCESS, payload: data};
}
function error(message) {
return {type:programsConstants.ERROR, payload:message};
}
reducers
import {programsConstants} from '../constants';
import React from "react";
const initialState = {
data: [],
loading: false,
error: null
};
export function programs(state = initialState, action) {
switch (action.type) {
case programsConstants.BEGIN:
return fetchPrograms(state);
case programsConstants.SUCCESS:
return populatePrograms(state, action);
case programsConstants.ERROR:
return fetchError(state, action);
case programsConstants.EXPANDED:
return programsExpanded(state, action);
default:
return state
}
}
function fetchPrograms(state = {}) {
return { ...state, data: [], loading: true, error: null };
}
function populatePrograms(state = {}, action) {
return { ...state, data: action.payload, loading: false, error: null };
}
function fetchError(state = {}, action) {
return { ...state, data: [], loading: false, error: action.payload };
}
component
import React from "react";
import { connect } from 'react-redux';
import { Route, Switch, Redirect } from "react-router-dom";
import { Header, Footer, Sidebar } from "../../components";
import dashboardRoutes from "../../routes/dashboard.jsx";
import Loading from "../../components/Loading/Loading";
import {loadPrograms} from "../../actions/programs.actions";
class Dashboard extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.dispatch(loadPrograms());
}
render() {
const { error, loading } = this.props;
if (loading) {
return <div><Loading loading={true} /></div>
}
if (error) {
return <div style={{ color: 'red' }}>ERROR: {error}</div>
}
return (
<div className="wrapper">
<Sidebar {...this.props} routes={dashboardRoutes} />
<div className="main-panel" ref="mainPanel">
<Header {...this.props} />
<Switch>
{dashboardRoutes.map((prop, key) => {
let Component = prop.component;
return (
<Route path={prop.path} component={props => <Component {...props} />} key={key} />
);
})}
</Switch>
<Footer fluid />
</div>
</div>
);
}
}
const mapStateToProps = state => ({
loading: state.programs.loading,
error: state.programs.error
});
export default connect(mapStateToProps)(Dashboard);
The component should receive updated props from the success dispatch and re render with the updated data. Currently the component only re renders on the begin dispatch and shows the loading component correctly but doesn't re render with the data is retrieved and updated to the state by the thunk.
I've researched this for a couple days and the generally accepted cause for the component not getting a state refresh is inadvertent state mutation rather than returning a new state. I don't think I'm mutating the state but perhaps I am.
Any help would much appreciated!
Update 1
As requested here's the code for creating the store and combining the reducers
store:
const loggerMiddleware = createLogger();
const composeEnhancers =
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(
thunk,
loggerMiddleware)
);
export const store = createStore(rootReducer, enhancer);
reducer combine:
import { combineReducers } from 'redux';
import { alert } from './alert.reducer';
import { programs } from './programs.reducer';
import { sidenav } from './sidenav.reducer';
const rootReducer = combineReducers({
programs,
sidenav,
alert
});
export default rootReducer;
The 2nd param is expected to be [preloadedState]:
export const store = createStore(rootReducer, {} , enhancer);
axios.get return a promise that you need to await for to get your data:
Try this:
export const loadPrograms = () => async (dispatch) => {
dispatch(programsActions.begin());
try {
const res = await axios.get('/programs/data');
const data = await res.data;
console.log('data recieved', data)
dispatch(programsActions.success(data.results));
} catch (error) {
dispatch(programsActions.error(error));
}
};
const mapStateToProps = state => ({
loading: state.programs.loading,
error: state.programs.error,
data: state.programs.data,
});
Action Call
import React from 'react';
import { connect } from 'react-redux';
import { loadPrograms } from '../../actions/programs.actions';
class Dashboard extends React.Component {
componentDidMount() {
// Try to call you action this way:
this.props.loadProgramsAction(); // <== Look at this
}
}
const mapStateToProps = state => ({
loading: state.programs.loading,
error: state.programs.error,
});
export default connect(
mapStateToProps,
{
loadProgramsAction: loadPrograms,
},
)(Dashboard);
After three days of research and refactoring, I finally figured out the problem and got it working. Turns out that the version of react-redux is was using (6.0.1) was the issue. Rolled back to 5.1.1 and everything worked flawlessly. Not sure if something is broken in 6.0.1 or if I was just using wrong.