useEffect runs for the old data - javascript

I am trying to write a singleton class that will act as a local storage wrapper. I need this because wherever I use localstorage in my whole app, I need each of the items I set to have prefix. This prefix for sure changes, but at only one place, so wrapper seems a good idea so that in my app, I don't have to pass prefix each time I use localStorage.
Here is my wrapper.
let instance;
class LocalStorage {
constructor() {
if(instance){
return instance;
}
instance = this;
instance.cachePrefix = null
}
_getKey(key, usePrefix) {
return usePrefix ? `${this.cachePrefix}:${key}` : key;
}
setPrefix(prefix) {
this.cachePrefix = prefix
}
set(key, value, usePrefix = true) {
if(key == null) {
return
}
localStorage.setItem(this._getKey(key, usePrefix),value)
}
get(key, usePrefix = true) {
return localStorage.getItem(this._getKey(key, usePrefix));
}
}
export const LocalStorageWrapper = new LocalStorage()
Now, where I import this class and call setPrefix, this piece is located in the very parent component tree, so we can say that this setPrefix will be called the first time.
Problem: Even though I call this setPrefix in the very parent, in that very parent, I have async call and when its result gets resolved, that's when I call setPrefix. Even though this is very fast, I am still not sure that this will work all the time.. It's possible that before this async call finishes, child component might start to render and it will try to use wrapper that won't have prefix set up...
I can't use hooks, because the whole react app is written with classes.
I'd appreciate your inputs what can be done here.
UPDATE THIS IS VERY PARENT PROVIDER IN THE TREE.
import React, { useContext, useEffect, useMemo, useState } from 'react'
import PropTypes from 'prop-types'
import BN from 'bn.js'
import { useWallet as useWalletBase, UseWalletProvider } from 'use-wallet'
import { getWeb3, filterBalanceValue } from './web3-utils'
import { useWalletConnectors } from './ethereum-providers/connectors'
import { LocalStorageWrapper } from './local-storage-wrapper'
const NETWORK_TYPE_DEFAULT = 'main'
const WalletContext = React.createContext()
function WalletContextProvider({ children }) {
const {
account,
balance,
ethereum,
connector,
status,
chainId,
providerInfo,
type,
...walletBaseRest
} = useWalletBase()
console.log("========= ", type);
const [walletWeb3, setWalletWeb3] = useState(null)
const [networkType, setNetworkType] = useState(NETWORK_TYPE_DEFAULT)
const connected = useMemo(() => status === 'connected', [status])
// get web3 and networkType whenever chainId changes
useEffect(() => {
let cancel = false
if (!ethereum) {
LocalStorageWrapper.setPrefix(NETWORK_TYPE_DEFAULT)
return
}
const walletWeb3 = getWeb3(ethereum)
setWalletWeb3(walletWeb3)
walletWeb3.eth.net
.getNetworkType()
.then(networkType => {
if (!cancel) {
setNetworkType(networkType)
LocalStorageWrapper.setPrefix(networkType)
}
return null
})
.catch(() => {
setNetworkType(NETWORK_TYPE_DEFAULT)
LocalStorageWrapper.setPrefix(NETWORK_TYPE_DEFAULT)
})
return () => {
cancel = true
setWalletWeb3(null)
setNetworkType(NETWORK_TYPE_DEFAULT)
LocalStorageWrapper.setPrefix(NETWORK_TYPE_DEFAULT)
}
}, [ethereum, chainId])
const wallet = useMemo(
() => ({
account,
balance: new BN(filterBalanceValue(balance)),
ethereum,
networkType: connected ? networkType : 'main',
providerInfo: providerInfo,
web3: walletWeb3,
status,
chainId,
connected,
...walletBaseRest,
}),
[
account,
balance,
ethereum,
networkType,
providerInfo,
status,
chainId,
walletBaseRest,
walletWeb3,
connected,
]
)
return (
<WalletContext.Provider value={wallet}>{children}</WalletContext.Provider>
)
}
WalletContextProvider.propTypes = { children: PropTypes.node }
export function WalletProvider({ children }) {
return (
<UseWalletProvider connectors={useWalletConnectors}>
<WalletContextProvider>{children}</WalletContextProvider>
</UseWalletProvider>
)
}
WalletProvider.propTypes = { children: PropTypes.node }
export function useWallet() {
return useContext(WalletContext)
}

Related

What is the reason getState() not functioning in React-Redux?

export const SMSAdmin_Filter_User = (userName) => (dispatch, getState) => {
var st = getState();
...
}
When this code runs, getState() is defined in the debugger as a function, but st comes up as undefined. I have used getState in multiple other action creator functions with great success, so am uncertain why it is not functioning here.
This function is called as a promise since there are other asynchronous processes running (incremental fetch for large number of users).
Here is where it is being called from:
componentDidMount() {
var el = document.getElementById("userList");
if (el) {
el.focus({ preventScroll: true });
}
// console.log("SMSAdmin")
new Promise((res,rej)=>this.props.SMSAdmin_Get_Users());
// .then(() => {
// this.setState({ users: this.props.SMSAdmin.users });
// });
}
filterUsers = () => {
let target = document.getElementById("userName");
let name = target?.value?.toLowerCase();
new Promise((res, rej)=>SMSAdmin_Filter_User(name?.trim()));
}
filterUsers() is also being called from the render function to ensure updates when SMSAdmin_Get_Users() adds more users to the list.
Here is the mapDispatchToProps():
const mapDispatchToProps = (dispatch) => {
return {
SMSAdmin_Get_Users: () => { return dispatch(SMSAdmin_Get_Users()) },
SMSAdmin_Load_User: (userId, userName, oldData = null, startVal = 0, number = 20) => {
return dispatch(SMSAdmin_Load_User(userId, userName, oldData, startVal, number))
},
SMSAdmin_Update_User: (user, province, credits) => { return dispatch(SMSAdmin_Update_User(user, province, credits)) },
SMSAdmin_setCurrentUpload: (userName) => { return dispatch(SMSAdmin_setCurrentUpload(userName)) },
SMSAdmin_Filter_User: (userName) => { return dispatch(SMSAdmin_Filter_User(userName)) },
}
}
I am not able to provide a sandbox for the code because there are multiple other files associated with this component and the data being used is confidential.
Thanks.
Edit: Showing redux store creation
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { reducers } from './reducerMain.js';
export const ConfigureStore = () => {
const store = createStore(
reducers,
applyMiddleware(thunk, logger)
);
return store;
}
I guess you are accidently using imported function (not the one mapped in mapDispatchToProps). Did you forgot to use the one from props ? like that:
filterUsers = () => {
// ...
new Promise((res, rej)=>this.props.SMSAdmin_Filter_User(name?.trim()));
}

React need to click twice to render data fetched from api

I need to click twice on a button linked to the current page so i can get the data fetched from the api to render. I am using nivo / charts to visualize my data.
The component fetches the company list from the api, and a second fetch loops through every result fetching data for every distinct company.
On first try, the company list were to fetch on the parent component, and a fetch request would take place for every child component thereafter
(parent=list of chart components, child=Company Chart), but on the pagination process it did not render properly so I had to uplifted the state to the parent component, the problem this time was that the parent component did not render on first click, I had to double click for example link button so that the parent component would render.
I thought the problem might be occurring since there might had been a missynchronization with the componentDidMount order of actions since I was sure that the first and second data fetching (first being the company get request and second distinct company get request), were executing simultaneously rather than one after the other. So I directed to redux and architectured my application to redux rules. It did not resolve anything and still requires to double click on a link so that the rendering would take place.
Now I feel like I would need to add some await/async rules for the api fetching process but I am not sure whether that would work or not, so I would really appreciate getting a second opinion on how to solve this little problem because it has been bugging me for weeks.
my Reducer:
import { FETCH_COMPANIES } from '../actions/types';
const initialState = {
next : null,
prev : null,
items : [],
item : [],
}
export default function(state = initialState, action) {
switch (action.type) {
case FETCH_COMPANIES:
return {
...state,
items : action.payload.companies,
next : action.payload.next,
prev : action.payload.prev,
}
default:
return state;
}
}
my Store.js:
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(...middleware)
)
)
export default store;
my Actions:
import axios from 'axios';
import { FloatType } from 'three';
import { FETCH_COMPANIES } from './types';
export const fetchAllData = (url) => dispatch => {
fetch(url)
.then(res => res.json())
.then(
posts =>
dispatch({
type : FETCH_COMPANIES,
payload : FetchCall(posts),
})
)
}
function FetchCall(res) {
let next;
let prev;
try {
next = res.next;
}
catch(err) {
console.log(err)
}
try {
prev = res.previous;
}
catch(err) {
console.log(err)
}
const CompanyArray = Array()
res.results.map(element => {
axios.get(`https://API/${element.symbol}/`).then((res) => {
const DataGroup = handleChartData(res.data)
CompanyArray.push({
'name' : element.symbol,
'data' : DataGroup,
})
})
});
const ALL_DATA = {
'next' : next,
'prev' : prev,
'companies' : CompanyArray,
}
return ALL_DATA;
}
function handleChartData(data) {
DataGroup = Object()
return DataGroup;
}
And my Component:
import React, { useState} from 'react';
import { Card, Row, Col, Button } from 'antd';
import Chart from '../components/Chart';
import DetailCompany from './CompanyDetail';
import { connect } from 'react-redux';
import { fetchAllData } from '../actions/chartActions';
import PropTypes from 'prop-types';
class CompanyList extends React.Component {
constructor(props) {
super(props);
this.state = {
charts : this.props.charts
}
}
componentWillMount() {
try {
this.props.fetchAllData("https://API/company/")
}
catch(err) {
console.log(err)
}
};
prevPage = () => {
let toPage = this.props.prev
this.props.fetchAllData(toPage)
}
nextPage = () => {
let toPage = this.props.next
this.props.fetchAllData(toPage)
}
render() {
const chartItems = this.state.charts.map(chart => (
<Col style={{margin:'0 0 75px 0'}} span={12} key={chart.name}>
<h1 style={{lineHeight:'2em', margin:'0 0 0 70px'}}>{chart.name}</h1>
<div className="chart-block">
<Chart symbol={chart.name}
data={chart.data.chartData}
>
</Chart>
</div>
</Col>
));
return (
<Card>
<Row>
{chartItems}
</Row>
<Row>
<Button disabled={(this.props.prev ? false : true )} onClick={() => {this.prevPage()}}>Previous</Button>
<Button onClick={() => {this.nextPage()}}>Next</Button>
</Row>
</Card>
)
}
}
CompanyList.propTypes = {
fetchAllData : PropTypes.func.isRequired,
charts : PropTypes.array.isRequired,
}
const mapStateToStore = state => ({
prev : state.charts.prev,
next : state.charts.next,
charts : state.charts.items,
});
export default connect(mapStateToStore, { fetchAllData })(CompanyList);
I would genuinely appreciate if anyone could help me to get around this problem and understand it to prevent further misdirection or reoccurrence. Thank you.
Your fetch thunk is not quite right. In particular, this line:
payload : FetchCall(posts),
FetchCall is asynchronous, but you aren't waiting for it to finish before dispatching. Within the FetchCall you are returning ALL_DATA with an empty CompanyArray before the axios calls finish.
You need to complete all fetch calls before returning or dispatching anything. You can do this with Promise/then, but I find it easier with async/await. Either way you need Promise.all to resolve the entire array. (Also I don't know why you use axios in one place and fetch in the other?).
// helper function to fetch the data for one company
const getCompanyData = async (symbol) => {
const res = await axios.get(`https://API/${symbol}/`);
return {
name: symbol,
data: res.data,
}
}
export const fetchAllData = (url) => async (dispatch) => {
const res = await axios.get(url);
const posts = res.data;
const {next, prev, results} = posts;
const companyArray = await Promise.all(
results.map( element => getCompanyData(element.symbol) )
);
dispatch({
type : FETCH_COMPANIES,
payload: {
next,
prev,
companyArray,
}
});
}
One of the issue that I noticed that the fetchcall is an async request so I think the companyarray would be a blank array in the payload. Are you sure you are getting payload.companies when FETCH_COMPANIES is dispatched?

MobX State Tree async actions and re-rendering React component

I am new to MST and is having a hard time finding more examples with async actions. I have an api that will return different data depending on the params you pass to it. In this case, the api can either return an array of photos or tutorials. I have set up my initial values for the store like so:
data: {
photos: [],
tutorials: []
}
Currently, I am using applySnapshot to update the store and eventually, that will trigger a re-render of my React component. In order to display both photos and tutorials, I need to call the api twice (Once with the params for photos and the second time for tutorials). I am running into an issue where the snapshot from the first update shows that photos and tutorials have the same values and only on the second update, do I get the correct values. I am probably misusing applySnapshot to re-render my React components. I would like to know the better/proper way of doing this. What is the best way to re-render my React components after the api has yielded a repsonse. Any suggestions are much appreciated
I have set up my store like this:
import { RootModel } from '.';
import { onSnapshot, getSnapshot, applySnapshot } from 'mobx-state-tree';
export const setupRootStore = () => {
const rootTree = RootModel.create({
data: {
photos: [],
tutorials: []
}
});
// on snapshot listener
onSnapshot(rootTree, snapshot => console.log('snapshot: ', snapshot));
return { rootTree };
};
I have created the following model with an async action using generators:
import {types,Instance,applySnapshot,flow,onSnapshot} from 'mobx-state-tree';
const TestModel = types
.model('Test', {
photos: types.array(Results),
tutorials: types.array(Results)
})
.actions(self => ({
fetchData: flow(function* fetchData(param) {
const results = yield api.fetch(param);
applySnapshot(self, {
...self,
photos: [... results, ...self.photos],
tutorials: [... results, ...self.tutorials]
});
})
}))
.views(self => ({
getPhoto() {
return self.photos;
},
getTutorials() {
return self.tutorials;
}
}));
const RootModel = types.model('Root', {
data: TestModel
});
export { RootModel };
export type Root = Instance<typeof RootModel>;
export type Test = Instance<typeof TestModel>;
React component for Photos.tsx
import React, { Component } from 'react';
import Spinner from 'components/Spinner';
import { Root } from '../../stores';
import { observer, inject } from 'mobx-react';
interface Props {
rootTree?: Root
}
#inject('rootTree')
#observer
class Photos extends Component<Props> {
componentDidMount() {
const { rootTree } = this.props;
if (!rootTree) return null;
rootTree.data.fetchData('photo');
}
componentDidUpdate(prevProps) {
if (prevProps.ctx !== this.props.ctx) {
const { rootTree } = this.props;
if (!rootTree) return null;
rootTree.data.fetchData('photo');
}
}
displayPhoto() {
const { rootTree } = this.props;
if (!rootTree) return null;
// calling method in MST view
const photoResults = rootTree.data.getPhoto();
if (photoResults.$treenode.snapshot[0]) {
return (
<div>
<div className='photo-title'>{'Photo'}</div>
{photoResults.$treenode.snapshot.map(Item => (
<a href={photoItem.attributes.openUrl} target='_blank'>
<img src={photoItem.url} />
</a>
))}
</div>
);
} else {
return <Spinner />;
}
}
render() {
return <div className='photo-module'>{this.displayPhoto()}</div>;
}
}
export default Photos;
Similarly, Tutorials.tsx is like so:
import React, { Component } from 'react';
import Spinner from '';
import { Root } from '../../stores';
import { observer, inject } from 'mobx-react';
interface Props {
rootTree?: Root;
}
#inject('rootTree')
#observer
class Tutorials extends Component<Props> {
componentDidMount() {
if (this.props.ctx) {
const { rootTree } = this.props;
if (!rootTree) return null;
rootTree.data.fetchData('tuts');
}
}
componentDidUpdate(prevProps) {
if (prevProps.ctx !== this.props.ctx) {
const { rootTree } = this.props;
if (!rootTree) return null;
rootTree.search.fetchData('tuts');
}
}
displayTutorials() {
const { rootTree } = this.props;
if (!rootTree) return null;
// calling method in MST view
const tutResults = rootTree.data.getTutorials();
if (tutResults.$treenode.snapshot[0]) {
return (
<div>
<div className='tutorials-title'>{'Tutorials'}</div>
{tutResults.$treenode.snapshot.map(tutorialItem => (
<a href={tutorialItem.attributes.openUrl} target='_blank'>
<img src={tutorialItem.url} />
</a>
))}
</div>
);
} else {
return <Spinner />;
}
}
render() {
return <div className='tutorials-module'>{this.displayTutorials()}</div>;
}
}
export default Tutorials;
Why are you using applySnapshot at all in this case? I don't think it's necessary. Just assign your data as needed in your action:
.actions(self => ({
//If you're fetching both at the same time
fetchData: flow(function* fetchData(param) {
const results = yield api.fetch(param);
//you need cast() if using Typescript otherwise I think it's optional
self.photos = cast([...results.photos, ...self.photos])
//do you really intend to prepend the results to the existing array or do you want to overwrite it with the sever response?
self.tutorials = cast(results.tutorials)
})
}))
Or if you need to make two separate requests to fetch your data it's probably best to make it two different actions
.actions(self => ({
fetchPhotos: flow(function* fetchPhotos(param) {
const results = yield api.fetch(param)
self.photos = cast([... results, ...self.photos])
}),
fetchTutorials: flow(function* fetchTutorials(param) {
const results = yield api.fetch(param)
self.tutorials = cast([... results, ...self.tutorials])
}),
}))
Regardless, it doesn't seem like you need applySnapshot. Just assign your data in your actions as necessary. There's nothing special about assigning data in an async action.

ReactRouter v4 Prompt - override default alert

The React Router v4 <Prompt></Prompt> component is perfect for the use case of protecting navigation away from a partially filled out form.
But what if we want to supply our own logic in place of the default browser alert() that this component uses? React is intended for creating UIs, so it seems like a pretty reasonable use case. Digging through the issues on Prompt in the github I did not find anyone asking about this.
Does anyone know of a solution for providing custom behavior for the alert?
Although you can make use of a custom Modal component while preventing navigating between pages through Links, you can't show a custom modal while trying to close browser or reload it.
However if thats fine with you, you can make use of history.listen to and block navigation. I wrote a generic HOC for it which solves this use case.
In the below code whitelisted pathnames are the pathnames that you would want the other person to navigate to without showing the prompt
import React from 'react';
import { withRouter } from 'react-router';
import _ from 'lodash';
const navigationPromptFactory = ({ Prompt }) => {
const initialState = {
currentLocation: null,
targetLocation: null,
isOpen: false
};
class NavigationPrompt extends React.Component {
static defaultProps = {
when: true
};
state = initialState;
componentDidMount() {
this.block(this.props);
window.addEventListener('beforeunload', this.onBeforeUnload);
}
componentWillReceiveProps(nextProps) {
const {
when: nextWhen,
history: nextHistory,
whiteListedPathnames: nextWhiteListedPaths
} = nextProps;
const { when, history, whiteListedPathnames } = this.props;
if (
when !== nextWhen ||
!_.isEqual(nextHistory.location, history.location) ||
!_.isEqual(whiteListedPathnames, nextWhiteListedPaths)
) {
this.unblock();
this.block(nextProps);
}
}
componentWillUnmount() {
this.unblock();
window.removeEventListener('beforeunload', this.onBeforeUnload);
}
onBeforeUnload = e => {
const { when } = this.props;
// we can't override an onBeforeUnload dialog
// eslint-disable-next-line
// https://stackoverflow.com/questions/276660/how-can-i-override-the-onbeforeunload-dialog-and-replace-it-with-my-own
if (when) {
// support for custom message is no longer there
// https://www.chromestatus.com/feature/5349061406228480
// eslint-disable-next-line
// https://stackoverflow.com/questions/38879742/is-it-possible-to-display-a-custom-message-in-the-beforeunload-popup
// setting e.returnValue = "false" to show prompt, reference below
//https://github.com/electron/electron/issues/2481
e.returnValue = 'false';
}
};
block = props => {
const {
history,
when,
whiteListedPathnames = [],
searchQueryCheck = false
} = props;
this.unblock = history.block(targetLocation => {
const hasPathnameChanged =
history.location.pathname !== targetLocation.pathname;
const hasSearchQueryChanged =
history.location.search !== targetLocation.search;
const hasUrlChanged = searchQueryCheck
? hasPathnameChanged || hasSearchQueryChanged
: hasPathnameChanged;
const isTargetWhiteListed = whiteListedPathnames.includes(
targetLocation.pathname
);
const hasChanged =
when && hasUrlChanged && !isTargetWhiteListed;
if (hasChanged) {
this.setState({
currentLocation: history.location,
targetLocation,
isOpen: true
});
}
return !hasChanged;
});
};
onConfirm = () => {
const { history } = this.props;
const { currentLocation, targetLocation } = this.state;
this.unblock();
// replacing current location and then pushing navigates to the target otherwise not
// this is needed when the user tries to change the url manually
history.replace(currentLocation);
history.push(targetLocation);
this.setState(initialState);
};
onCancel = () => {
const { currentLocation } = this.state;
this.setState(initialState);
// Replacing the current location in case the user tried to change the url manually
this.unblock();
this.props.history.replace(currentLocation);
this.block(this.props);
};
render() {
return (
<Prompt
{...this.props}
isOpen={this.state.isOpen}
onCancel={this.onCancel}
onConfirm={this.onConfirm}
/>
);
}
}
return withRouter(NavigationPrompt);
};
export { navigationPromptFactory };
In order to use the above, you can simply provide your custom Prompt Modal like
const NavigationPrompt = navigationPromptFactory({
Prompt: AlertDialog
});
const whiteListedPathnames = [`${match.url}/abc`, match.url];
<NavigationPrompt
when={isEditingPlan}
cancelLabel={'Stay'}
confirmLabel={'Leave'}
whiteListedPathnames={whiteListedPathnames}
title={'Leave This Page'}
>
<span>
Unsaved Changes may not be saved
</span>
</NavigationPrompt>
The prompt component by default doesn't allow overriding the use of window.alert().
Here's a link to a conversation that matches your needs fairly similarly:
https://github.com/ReactTraining/react-router/issues/4635
There's a few key points in there that you can refer to, mostly just that instead of using prompt you can just make your own modal to be triggered on specific user actions. :)
Hope this helps
Here's a component using hooks to achieve block functionality, the <Prompt.../> component didn't work for me because I wanted to ignore the search on the location.
import { useEffect, useRef } from 'react';
import { useHistory } from 'react-router-dom';
interface IProps {
when: boolean;
message: string;
}
export default function RouteLeavingGuard({ when, message }: IProps) {
const history = useHistory();
const lastPathName = useRef(history.location.pathname);
useEffect(() => {
const unlisten = history.listen(({ pathname }) => lastPathName.current = pathname);
const unblock = history.block(({ pathname }) => {
if (lastPathName.current !== pathname && when) {
return message;
}
});
return () => {
unlisten();
unblock();
}
}, [history, when, message]);
return null;
}

React Redux state array changes are not re-rendering a component

I have a project that uses React + Redux + Thunk and I am rather new to the stack. I have a scenario where I am fetching an array from an API call in my action/reducer, but it is not re-rendering in a component/container that is hooked up to the Store. The component does render the first time when I fire up the app, but at that point the array is undefined when logged to console.
I am trying to display the array's length, so this is always resulting in 0. With ReduxDevTools I see that the state of network_identities does populate correctly and is longer zero... Where am I going wrong?
Here is my sample action
///////////// Sample action /////////////
import axios from 'axios';
const url = 'sample#url.com';
const authorization = 'sample_auth';
export function fetchConnections() {
const params = {
headers: {
authorization,
},
};
return (dispatch) => {
// call returns an array of items
axios.get(`${url}/connection`, params)
.then((connections) => {
let shake_profiles = [];
let connected_profiles = [];
let entity_res;
// map through items to fetch the items data, and split into seperate arrays depending on 'status'
connections.data.forEach((value) => {
switch (value.status) {
case 'APPROVED': case 'UNAPPROVED':
{
axios.get(`${url}/entity/${value.entity_id_other}`, params)
.then((entity_data) => {
entity_res = entity_data.data;
// add status
entity_res.status = value.status;
// append to connected_profiles
connected_profiles.push(entity_res);
});
break;
}
case 'CONNECTED':
{
axios.get(`${url}/entity/${value.entity_id_other}`, params)
.then((entity_data) => {
entity_res = entity_data.data;
entity_res.status = value.status;
shake_profiles.push(entity_res);
})
.catch(err => console.log('err fetching entity info: ', err));
break;
}
// if neither case do nothing
default: break;
}
});
dispatch({
type: 'FETCH_CONNECTIONS',
payload: { shake_profiles, connected_profiles },
});
});
};
}
Sample Reducer
///////////// Sample reducer /////////////
const initialState = {
fetched: false,
error: null,
connections: [],
sortType: 'first_name',
filterType: 'ALL',
shake_identities: [],
network_identities: [],
};
const connectionsReducer = (state = initialState, action) => {
switch (action.type) {
case 'FETCH_CONNECTIONS':
console.log('[connections REDUCER] shake_profiles: ', action.payload.shake_profiles);
console.log('[connections REDUCER] connected_profiles: ', action.payload.connected_profiles);
return { ...state,
fetched: true,
shake_identities: action.payload.shake_profiles,
network_identities: action.payload.connected_profiles,
};
default:
return state;
}
};
export default connectionsReducer;
Sample Store
///////////// Sample Store /////////////
import { applyMiddleware, createStore, compose } from 'redux';
import thunk from 'redux-thunk';
import promise from 'redux-promise-middleware';
import reducers from './reducers';
const middleware = applyMiddleware(promise(), thunk);
// Redux Dev Tools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, composeEnhancers(middleware));
export default store;
Sample Component - see if the API is done fetching the array, then display the length of the array
///////////// Sample Component /////////////
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import CSSModules from 'react-css-modules';
import * as ConnectionActions from 'actions/connections';
import styles from './styles.scss';
function mapStateToProps(state) {
return {
network_identities: state.connections.network_identities,
loadedConnections: state.connections.fetched,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Object.assign({}, ConnectionActions), dispatch),
};
}
class Counter extends Component {
componentWillMount() {
const { network_identities, actions } = this.props;
if (!network_identities.length) {
console.log('||| fetching Connections');
actions.fetchConnections();
}
}
render() {
let { network_identities, loadedConnections} = this.props;
console.log('[Counter] network_identities[0]: ', network_identities[0]);
console.log('[Counter] network_identities: ', network_identities);
console.log('[Counter] loadingConnections: ', loadingConnections);
return (
<div>
<Link to="/network">
<div>
<span>Connections</span>
{ !loadedConnections ? (
<span><i className="fa fa-refresh fa-spin" /></span>
) : (
<span>{network_identities.length}</span>
) }
</div>
</Link>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CSSModules(Counter, styles));
I suspect I am either mutating the state in my reducer, or I am misusing Thunk.
The problem in the code is that connections.data.forEach((value) => {..}) will send out a bunch of fetches, and then immediately return without waiting for the result arrays to be populated. A 'FETCH_CONNECTIONS' action is dispatched with empty arrays, and all connected components will rerender with the empty results.
What makes it tricky though is that the array objects that you put in the store will get pushed to once the fetches finish, so when you inspect the store it will seem populated correctly.
Not using any mutations will prevent the accidental population of the store, but won't solve the fact that dispatch is fired before the results are in. To do that, you could either create actions to add single results and dispatch those in the axios.get().then parts, or you could create a list of promises and wait for all of them to resolve with Promise.all().
Here's what the latter solution could look like.
axios.get(`${url}/connection`, params)
.then((connections) => {
const connectionPromises = connections.data.map((value) => {
switch (value.status) {
case 'APPROVED': case 'UNAPPROVED':
return axios.get(`${url}/entity/${value.entity_id_other}`, params)
.then((entity_data) => {
return {connected_profile: {...entity_data.data, status: value.status}};
});
case 'CONNECTED':
return axios.get(`${url}/entity/${value.entity_id_other}`, params)
.then((entity_data) => {
return {shake_profile: {...entity_data.data, status: value.status}};
})
// if neither case do nothing
default:
return {};
}
});
Promise.all(connectionPromises)
.then((connections) => {
const connected_profiles =
connections.filter((c) => c.connected_profile).map((r) => r.connected_profile);
const shake_profiles =
connections.filter((c) => c.shake_profile).map((r) => r.shake_profile);
dispatch({
type: 'FETCH_CONNECTIONS',
payload: { shake_profiles, connected_profiles },
});
}).catch(err => console.log('err fetching entity info: ', err));
});
You'll probably want to use some more appropriate names though, and if you use lodash, you can make it a bit prettier.
The issue here is that you are making an async operation within a componentWillMount. When this lifecycle method is called,it does not block the render method from being called. That is, it does not wait until there is a response from its operations. So, rather move this async action to componentDidMount.

Categories

Resources