Loading HOC with Thunk problem with props propagation - reactjs

I created an HOC to deal with loading. It uses a property isLoading to decide what to show.
When I open a page directly from the URL I haven't any issue and everything works fine (because isLoading property is setted up on true by default).
The problem is that when I click on a link (<Link ... />) things doesn't work as expected because isLoading is now false because the page where I am navigating from setted the state to this value.
So, this is the HOC:
import React from 'react';
export default function WithLoading(WrappedComponent, loadingDelegate) {
class LoadingComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true
};
this.loadingDelegate = loadingDelegate;
}
componentDidMount() {
loadingDelegate(this);
}
render() {
if (this.props.isLoading === true) {
return (
<div>Loading...</div>
);
} else {
return <WrappedComponent {...this.props} />;
}
}
}
return LoadingComponent;
}
And below the component:
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from './SurveysStore';
import WithLoading from '../LoadingHOC';
//edit/:id
class SurveyDetailRoutedComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
{this.props.survey.questions.length}
</div>
);
}
}
const SurveyDetailRoutedComponentWithLoading = WithLoading(SurveyDetailRoutedComponent, (_context) => _context.props.requestSurvey(parseInt(_context.props.match.params.id)));
export default connect(
state => state.surveysReducer,
dispatch => bindActionCreators(actionCreators, dispatch)
)(SurveyDetailRoutedComponentWithLoading);
The error I have is that survey is null, because I tried to render a not already resolver property.
The problem occurs only when I render this component via a <Link> contained in a page rendered in the same way.
I tried to set isLoading=true on the routing, but this doesn't work for me:
export default class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Layout>
<Route exact path='/' component={(props) => <Home {...props} />} />
<Route path='/survey/edit/:id' component={(props) => <SurveyDetailRoutedComponent {...props} isLoading={true} />} />
</Layout>
);
}
}
The store itself is very simple and I imagine my error is on how I deal with redux, because it looks like the routing operation is not resetting the loading.
const resetLoading = "RESET_LOADING";
const requestSurveys = "REQUEST_SURVEYS";
const receiveSurveys = "RECEIVE_SURVEYS";
const requestSurvey = "REQUEST_SURVEY";
const receiveSurvey = "RECEIVE_SURVEY";
const initialState = { surveys: [], isLoading: true, survey: null };
export const actionCreators = {
resetLoading: () => function (dispatch, getState) {
dispatch({ type: resetLoading })
},
requestSurveys: () => async function (dispatch, getState) {
dispatch({ type: requestSurveys });
const response = await fetch(...)
const responseAsJson = await response.json();
dispatch({ type: receiveSurveys, surveys: responseAsJson.data.surveys });
},
requestSurvey: id => async function (dispatch, getState) {
dispatch({ type: requestSurvey });
const response = await fetch(...)
const responseAsJson = await response.json();
dispatch({ type: receiveSurvey, survey: responseAsJson.data.survey });
}
};
export const reducer = function(state, action) {
state = state || initialState;
switch (action.type) {
case resetLoading:
return {
...state,
isLoading: true
};
case requestSurveys:
return {
...state,
isLoading: true
};
case requestSurvey:
return {
...state,
isLoading: true
};
case receiveSurveys:
return {
...state,
isLoading: false,
surveys: action.surveys
};
case receiveSurvey:
return {
...state,
isLoading: false,
survey: action.survey
};
default:
return state;
}
}

Related

Props not displaying from fetch call

I am trying to display recipes and not sure if I have this setup correctly. I am pulling recipes from a rails api via get fetch request. At the moment nothing is displaying.
Here is my recipe container:
import React, { Component } from 'react'
import RecipeList from '../components/RecipeList'
import RecipeInput from '../components/RecipeInput'
import { connect } from 'react-redux'
import { postRecipes } from '../actions/postRecipes.js'
import { getRecipes } from '../actions/getRecipes'
class RecipeContainer extends Component{
constructor(props){
super(props)
}
componentDidMount(){
getRecipes()
}
render(){
return (
<div>
<RecipeInput postRecipes={this.props.postRecipes} />
<RecipeList getRecipes={this.props.recipes} />
</div>
)
}
}
const mapStateToProps = state =>({
recipes: state.recipes
})
const mapDispatchToProps = dispatch =>{
return{
postRecipes: (recipe) => dispatch(postRecipes(recipe)),
getRecipes: () => dispatch(getRecipes())
// deleteRecipe: id => dispatch({type: 'Delete_Recipe', id})
}
}
export default connect(mapStateToProps,mapDispatchToProps)(RecipeContainer)
Here is my get request....notice that I am returning my Recipe component here.
export const getRecipes = () => {
const BASE_URL = `http://localhost:10524`
const RECIPES_URL =`${BASE_URL}/recipes`
return (dispatch) => {
dispatch({ type: 'START_FETCHING_RECIPES_REQUEST' });
fetch(RECIPES_URL)
.then(response =>{ return response.json()})
.then(recipes => dispatch({ type: 'Get_Recipes', recipes }));
};
}
This is where I am trying to render the Recipe component from the get request
import React, {Component} from 'react';
// import { getRecipes } from '../actions/getRecipes.js';
import Recipe from './Recipe.js'
class RecipeList extends Component {
// componentDidMount(){
// getRecipes()
// }
render() {
return (
<div>
{this.props.recipes.map(recipe => (<Recipe recipe={recipe} key={recipe.id} />))}
</div>
)
}
}
export default RecipeList;
Edit: Added reducer
switch(action.type){
case 'Add_Recipe':
const recipe = {
name: action.name,
ingredients: action.ingredients,
chef_name: action.chef_name,
origin: action.origin,
category: action.category
}
return{
...state,
recipes: [...state.recipes, recipe],
}
case 'START_FETCHING_RECIPES_REQUEST':
return {
...state,
recipes: [...state.recipes],
requesting: true
}
case 'Get_Recipes':
return {
...state, recipes: action.recipes,
requesting: false
}
default:
return state
}
}
How can I correct this to make it work?
Issue
You are not passing the recipes to the RecipeList component that were fetched and presumably stored in state, and fed back to the UI via RecipeContainer.
Solution
Pass the recipe state from RecipeContainer to RecipeList as a prop. and then render/map the recipes from props.
RecipeContainer
class RecipeContainer extends Component{
componentDidMount() {
getRecipes();
}
render() {
return (
<div>
<RecipeInput postRecipes={this.props.postRecipes} />
<RecipeList getRecipes={this.props.recipes} /> // <-- pass recipe state
</div>
)
}
}
const mapStateToProps = state => ({
recipes: state.recipes,
});
const mapDispatchToProps = dispatch => {
return {
postRecipes: (recipe) => dispatch(postRecipes(recipe)),
getRecipes: () => dispatch(getRecipes())
}
};
RecipeList
class RecipeList extends Component {
render() {
const { recipes } = this.props;
return (
<div>
{recipes.map(recipe => (
<Recipe recipe={recipe} key={recipe.id} />
))}
</div>
);
}
}
The actual solution to this was I needed to have an explicit return in my mapStateToProp function.
Eg.
const mapStateToProp = state =>{
return {
recipes: state.recipes
}
}

mapStateToProps returns no data, data is undefined

My problem is that mapStateToProps returns undefined. Maybe I have some problems with dispatching in the state or maybe app rendering before data comes from the server? I can't understand. So app works right without redux with just componentDidMount, but I have some problems with redux
So I have a top-level component App:
const App = () => {
return (
<Provider store={store}>
<Screen />
</Provider>
)
}
I have store with thunk meddleware:
const store = createStore(reducer, applyMiddleware(ReduxThunk));
Two types of action:
export const fetchData = (newPhotos) => async (dispatch) => {
function onSuccess(success) {
dispatch({
type: FETCH_DATA,
payload: success})
return success
}
function onError(error) {
dispatch({type: FETCH_FAILED, error})
}
try {
const URL = 'https://api.unsplash.com/photos/?client_id=cf49c08b444ff4cb9e4d126b7e9f7513ba1ee58de7906e4360afc1a33d1bf4c0';
const res = await fetch(URL);
const success = await res.json();
console.log(success);
return onSuccess(success);
} catch (error) {
return onError(error)
}
};
reducer:
const initialState = {
data: []
}
export default dataReducer = (state = initialState, action) => {
console.log(action);
switch (action.type) {
case FETCH_DATA:
return {
data: action.payload
}
case FETCH_FAILED:
return {
state
}
default: return state;
}
}
combine reducers:
export default combineReducers({
fetchedData: dataReducer
});
and my rendering component:
class HomeScreen extends Component {
render() {
console.log(this.props)
const {navigation, data} = this.props;
return (
<ScrollView>
<Header />
<ImageList navigation={navigation} data={data}/>
</ScrollView>
)
}
}
const mapStateToProps = state => {
return {
data: state.fetchedData.data
}
}
export default connect(mapStateToProps, {fetchData})(HomeScreen);
fetchData action will not be called on its own. You need to call that explicitly(in componentDidMount and, probably, componentDidUpdate) like
class HomeScreen extends Component {
componentDidMount() {
this.props.fetchData(/* I don't know where you are going to take newPhotos argument */);
}
render() {
//...
}
}

React-Redux mapStateToProps doesn't trigger when state changes

I'm learning Redux state management and got stuck with an issue. The mapStateToProps within a component is not triggered when the state changes. Gone through a lot of blogs, couldn't able to figure out the problem.
The store seems to update properly, as the "subscribe" method on store prints new changes. Attached screenshot as well.
Actions.js
export const GET_ITEMS_SUCCESS = "GET_ITEMS_SUCCESS";
export const GET_ITEMS_FAILURE = "GET_ITEMS_FAILURE";
export const getItemsSuccess = (items) => ({
type: GET_ITEMS_SUCCESS, payload: items
});
export const getItemsFailure = (error) => ({
type: GET_ITEMS_FAILURE, error: error
});
export function getItems(dispatch) {
return dispatch => {
fetch(myList)
.then(res => res.json())
.then(res => {
if(res.error) {
throw(res.error);
}
store.dispatch(getItemsSuccess(res));
return res;
})
.catch(error => {
store.dispatch(getItemsFailure(error));
})
}
}
Reducer
let initialState = {items: [], error: null}
function GetItemsReducer (state = initialState, action) {
switch (action.type) {
case GET_ITEMS_SUCCESS:
return Object.assign({}, state, {pending: false, items: action.payload});
case GET_ITEMS_FAILURE:
return Object.assign({}, state, {pending: false, error: action.error});
default:
return state;
}
}
export default const rootReducer = combineReducers({
GetItemsReducer: GetItemsReducer
});
Store
const mystore = createStore(rootReducer, applyMiddleware(thunk, promise));
mystore.subscribe(() => console.log("State Changed;", mystore.getState()));
Component
class Home extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.fetchItems();
}
render() {
return (
<div>{this.props.items.length}</div>
)
}
}
const mapStateToProps = (state) => {
console.log('mapStateToProps ----------> ', state);
return {
items: state.GetItemsReducer.items,
error: state.GetItemsReducer.error
}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchItems: bindActionCreators(getItems, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
Main
class App extends React.Component {
render() {
return (
<Provider store={mystore}>
<Home />
</Provider>
)
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
Thanks in advance.

Component state reverts back to default before correcting itself very fast

I have a small issue that I can't figure out why it happens. After signing in, if the user attempts to access the login page, it will redirect to the home page. However, I'm not sure why my /signin page flashes/appears for half a second before the redirect happens.
I'll try my best to explain the flow of the code:
Whenever a user logins, state.signIn.signInSuccess is set to true. This state is mapped to a prop in my routes component, where I pass it down as a prop to my AuthorizedRoutes component. The authorizedRoutes component check if signInSuccess is true or not and will render the /signin page or redirect. When I try to access the signin page I console.log the state, signInSuccess shows up as false for half a second before changing to true even though the user is already signed in. It's a small issue, but I want to understand why the state is changing to false for that half a second.
If anyone could help me, I'd appreciate it. Thanks
////signin action creators////
import { firebaseApp } from '../firebase.js';
import { SIGNIN_PENDING, SIGNIN_SUCCESS, SIGNIN_FAIL } from '../constants/signin.js';
const setSignInPending = signInPending => {
return{
type:SIGNIN_PENDING,
signInPending
};
};
const setSignInSuccess = signInSuccess => {
return{
type:SIGNIN_SUCCESS,
signInSuccess
};
};
const setSignInFail = signInError => {
return{
type:SIGNIN_FAIL,
signInError
};
};
export const signIn = (email, password) => {
return dispatch => {
dispatch(setSignInPending(true));
return firebaseApp.auth().signInWithEmailAndPassword(email, password)
.then(res => {
dispatch(setSignInPending(false));
dispatch(setSignInSuccess(true));
})
.catch(error => {
console.log('error', error);
dispatch(setSignInPending(false));
dispatch(setSignInFail(error.code));
})
}
};
export const signedIn = () => {
return dispatch => {
dispatch(setSignInSuccess(true));
dispatch(setSignInFail(null));
};
};
////signin reducer////
import { SIGNIN_PENDING, SIGNIN_SUCCESS, SIGNIN_FAIL } from '../constants/signin.js';
const defaultState = {
signInPending: false,
signInSuccess: false,
signInError: null,
};
const signIn = (state = defaultState, action) => {
let signInState = null;
switch (action.type){
case SIGNIN_PENDING:
signInState = {
...state,
signInPending: action.signInPending
};
return signInState;
case SIGNIN_SUCCESS:
signInState = {
...state,
signInSuccess: action.signInSuccess,
};
return signInState;
case SIGNIN_FAIL:
signInState = {
...state,
signInError: action.signInError
};
return signInState;
default:
return state;
};
};
export default signIn;
////Routes component////
class Routes extends React.Component{
constructor(props){
super(props);
}
componentWillMount(){
firebaseApp.auth().onAuthStateChanged(user => {
// user ? dispatch(setSignedInTrue()) : dispatch(setSignedInFalse());
if (user){
this.props.signedIn();
}
});
}
render(){
return(
<Router>
<Switch>
<Route exact path='/' render={(props) => (<App signInSuccess={this.props.signInSuccess} {...props}/>)} />
<AuthorizedRoute signedIn={this.props.signInSuccess} path="/signup" component={SignUp} />
<AuthorizedRoute signedIn={this.props.signInSuccess} path="/signin" component={SignIn} />
<Route component={InvalidPage} />
</Switch>
</Router>
);
}
};
Routes.propTypes = {
signedIn: PropTypes.func.isRequired
};
const mapStateToProps = (state) => {
return{
signInSuccess: state.signIn.signInSuccess
};
};
const mapDispatchToProps = (dispatch) => {
return{
signedIn: () => dispatch(signedIn()),
signOut: () => dispatch(signOut())
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Routes);
////AuthorizedRoutes Component////
class AuthorizedRoute extends React.Component{
constructor(props){
super(props);
};
render(){
console.log(this.props);
const { component: Component, ...rest } = this.props;
return(
<Route {...rest} render={props => {
if (this.props.signedIn){
return <Redirect to="/" />
} else if (this.props.signedIn === false){
return <Component {...props} />
} else{
return null;
}
}} />
);
}
};
AuthorizedRoute.propTypes = {
signInSuccess: PropTypes.bool
};
export default AuthorizedRoute;
You trigger 2 separate Redux state changes:
dispatch(setSignInPending(false));
dispatch(setSignInSuccess(true));
those will each cause a re-render, so when you setSignInPending(false), there is a render where signInPending is false but setSignInSuccess(true) hasn't been called.
This is a common usage of Redux, to use it with simple getters/setters. Think of it more like events which occur which can mutate multiple aspects of the state. I think you actually only have 1 event: signInSuccess (or fail), and you simply need to set pending to false in that reducer case because you know you got a response. That way there is only 1 Redux state change and when the render is called the state isn't in an invalid state as it is now.
////signin reducer////
import { SIGNIN_SUCCESS, SIGNIN_FAIL } from '../constants/signin.js';
const defaultState = {
signInPending: false,
signInSuccess: false,
signInError: null,
};
const signIn = (state = defaultState, action) => {
let signInState = null;
switch (action.type){
case SIGNIN_SUCCESS:
signInState = {
...state,
signInSuccess: action.signInSuccess,
signInPending: false,
signInError: null
};
return signInState;
case SIGNIN_FAIL:
signInState = {
...state,
signInError: action.signInError,
signInPending: false,
signInSuccess: false
};
return signInState;
default:
return state;
};
};
export default signIn;

React Redux new data replacing current data instead of extend it and function only run once

I'm using this package https://github.com/RealScout/redux-infinite-scroll to make infinite scroll on list of brand. Here is my code:
Container:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions, getBrands } from '../reducer';
import Infinite from 'react-infinite';
import InfiniteScroll from 'redux-infinite-scroll';
import SearchBox from '../components/SearchBox';
import CardList from '../components/CardList';
const { fetchBrands } = actions;
class BrandList extends Component {
componentDidMount() {
this.props.fetchBrands({ page: 1 });
}
renderList() {
const brands = this.props.brands;
return brands.map((brand) => {
return (
<CardList key={brand.id} name={brand.name} avatar={brand.avatar.thumbnail} follower={brand.follows_count} />
);
});
}
toggle() {
return this.props.isFetching;
}
loadMore() {
const {lastPage, currentPage} = this.props;
const nextPage = currentPage ? parseInt(currentPage) + 1 : 1;
if(currentPage && currentPage <= lastPage){
this.props.fetchBrands({page: nextPage});
}
}
render() {
return (
<div>
<SearchBox />
<div className="row">
<InfiniteScroll
items={this.renderList()}
loadMore={this.loadMore.bind(this)}
/>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
brands: getBrands(state),
isFetching: state.brand.isFetching,
currentPage: state.brand.currentPage,
lastPage: state.brand.lastPage
};
}
export default connect(mapStateToProps, { fetchBrands })(BrandList);
Reducer:
import axios from 'axios';
// Define Types
export const types = {
// brand list
FETCH_BRANDS: 'fetch_brands',
FETCH_BRANDS_SUCCESS: 'fetch_brands_success',
FETCH_BRANDS_ERROR: 'fetch_brands_failure',
FETCH_BRAND: 'fetch_brand',
FETCH_BRAND_SUCCESS: 'fetch_brand_success',
FETCH_BRAND_ERROR: 'fetch_brand_failure',
};
const { FETCH_BRANDS, FETCH_BRANDS_SUCCESS, FETCH_BRANDS_ERROR } = types;
// Define Reducer
export const INITIAL_STATE = { brands: [], brand: {}, isFetching: false, error: null, currentPage: 1 };
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_BRANDS:
return { ...state, isFetching: true };
case FETCH_BRANDS_SUCCESS:
return { ...state, brands: action.payload.brands.data, currentPage: action.payload.brands.current_page, lastPage: action.payload.brands.last_page };
case FETCH_BRANDS_ERROR:
return { ...state, error: action.payload };
default:
return state;
}
}
// Define Actions
export const actions = {
fetchBrands: ({page, count = 15}) => {
return (dispatch) => {
dispatch({ type: FETCH_BRANDS });
axios.get(`brands?page=${page}&count=${count}`)
.then((response) => {
const {data} = response;
if (data.code == 200) {
dispatch({ type: FETCH_BRANDS_SUCCESS, payload: data });
}
});
};
}
};
// SELECTOR
export const getBrands = (state) => state.brand.brands;
it run loadMore function successfully but it not extend current list, it replace it instead.
loadmore function only run once. it should run 10 times.
do I miss something on my code to make it scroll?
Try adding
brands: [ ...state.brands, ...action.payload.brands.data]
like this in your reducer
case FETCH_BRANDS_SUCCESS:
return { ...state, brands: [ ...state.brands, ...action.payload.brands.data], currentPage: action.payload.brands.current_page, lastPage: action.payload.brands.last_page };
Which means that you are concating current list with upcoming list (versioned data)

Resources