MobX State Tree async actions and re-rendering React component - javascript

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.

Related

Unable to pass state from reducer to 'this.props', fetching API using axios on React-Redux

Hi I'm very new to React and this is my first project, what I'm trying to do is when onClick, the code will fetch data from API using Axios and display the object.
First, I tried using a reducer and failed. Read on Redux docs, that the way to resolve when dealing with asynchronous actions we to use applymiddleware. So after trying and adjusting I end up with:
// I KNOW... YOU MIGHT BE THINKING WHY REDUX FOR SUCH A SIMPLE APP, WHY NOT JUST USE REACT?? IT'S SUCH AN OVER-KILL. WELL... I FORCE MYSELF TO IMPLEMENT IT ON THE FIRST PROJECT TO UNDERSTAND AND DEMONSTRATE THE FUNCTIONALITY OF REACT, REDUX AND REACT-REDUX. SIMPLE APP IS IT CLEARER/OBVIOUS.
const thunk = ReduxThunk.default;
const { Provider, connect } = ReactRedux;
const { createStore, applyMiddleware } = Redux;
const GENERATE = 'GENERATE';
class QuoteGenerator extends React.Component {
generateQuote = () => {
this.props.dispatch(generateNewQuote());
};
generate = () => {
this.props.dispatch({
type: GENERATE,
});
};
render() {
console.log(this.props);
return (
<div>
<h1>{this.props.quote}</h1>
<button onClick={this.generate}>Generate</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
quote: state.quote,
});
const Container = connect(mapStateToProps)(QuoteGenerator);
/**************************************************************/
const initState = {
quote: 'Generate Me!',
};
function fetchQuote() {
return axios.get(
'https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json'
);
}
function returnQuote(quote) {
return {
quote: quote,
};
}
// But what do you do when you need to start an asynchronous action,
// such as an API call, or a router transition?
// Meet thunks.
// A thunk is a function that returns a function.
// This is a thunk.
function generateNewQuote(state = initState, action) {
// if (action.type === GENERATE) {
// console.log("It Works")
// Invert control!
// Return a function that accepts `dispatch` so we can dispatch later.
// Thunk middleware knows how to turn thunk async actions into actions.
return function (dispatch) {
return fetchQuote().then((res) => {
const selectRandomQuote = Math.floor(
Math.random() * res.data.quotes.length
);
const quoteObj = {
quote: res.data.quotes[selectRandomQuote]['quote'],
author: res.data.quotes[selectRandomQuote]['author'],
};
console.log({ quote: res.data.quotes[selectRandomQuote]['quote'] });
return { quote: res.data.quotes[selectRandomQuote]['quote'] };
});
};
// } else {
// return state;
// }
}
function reducer(state = initState, action) {
if (action.type === GENERATE) {
return {
quote: 'It Works!',
};
} else {
return state;
}
}
// applyMiddleware supercharges createStore with middleware:
const store = createStore(generateNewQuote, applyMiddleware(thunk));
class App extends React.Component {
render() {
return (
<Provider store={store}>
<Container />
</Provider>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
I'm able to trigger the function and console log the output but with no luck, it's not showing...
I really appreciate the guidance to share some light on this, been really keen on React but the first has hit me hard. Thanks in advance, cheers!

useEffect runs for the old data

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)
}

Why am I getting the error data.findIndex is not a function in my React with Redux project?

I have an ASP.NET Core project with React and Redux, I'm also using the Kendo React UI. I'm trying to return data to one of my Kendo widgets but I'm getting an error when I try to do so and I need help identifying what I've done wrong.
When I run my application I get the error of:
1 of 2 errors on the page TypeError: data.findIndex is not a function
DropDownList/_this.renderDropDownWrapper
C:/Users/Allan/node_modules/#progress/kendo-react-dropdowns/dist/es/DropDownList/DropDownList.js:83
80 | var focused = _this.state.focused; 81 | var opened =
_this.props.opened !== undefined ? _this.props.opened : _this.state.opened; 82 | var value = _this.value;
83 | var selectedIndex = data.findIndex(function (i) { return areSame(i, value, dataItemKey); }); 84 | var text =
getItemValue(value, textField); 85 | var valueDefaultRendering =
(React.createElement("span", { className: "k-input" }, text)); 86 |
var valueElement = valueRender !== undefined ?
In the console this error shows as:
Warning: Failed prop type: Invalid prop data of type string
supplied to DropDownList, expected array.
The error makes sense, but the data I'm returning should be an array. It's not though as it doesn't appear to return anything. So I've done something wrong.
Here is my code so far, please note that my data is served from a generic repository.
components/vessels/WidgetData.js
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { actionCreators } from '../../store/Types';
import { DropDownList } from '#progress/kendo-react-dropdowns';
class WidgetData extends Component {
state = {
vesseltypes: ""
};
componentWillMount() {
this.props.requestTypes();
}
render() {
return (
<div>
<DropDownList data={this.state.vesseltypes} />
</div>
);
}
}
export default connect(
state => state.vesseltypes,
dispatch => bindActionCreators(actionCreators, dispatch)
)(WidgetData);
components/store/Types.js
const requestVesselTypes = 'REQUEST_TYPES';
const receiveVesselTypes = 'RECEIVE_TYPES';
const initialState = {
vesseltypes: [],
isLoading: false
};
export const actionCreators = {
requestTypes: () => async (dispatch) => {
dispatch({ type: requestVesselTypes });
const url = 'api/KendoData/GetVesselTypes';
const response = await fetch(url);
const alltypes = await response.json();
dispatch({ type: receiveVesselTypes, alltypes });
}
}
export const reducer = (state, action) => {
state = state || initialState;
if (action.type === requestVesselTypes) {
return {
...state,
isLoading: true
};
}
if (action.type === receiveVesselTypes) {
alltypes = action.alltypes;
return {
...state,
vesseltypes: action.alltypes,
isLoading: false
}
}
return state;
};
And finally, the reducer is defined in the store
components/store/configureStore.js
const reducers = {
vesseltypes: Types.reducer
};
I've tested the API to ensure data is there and it works, I've logged said data to the console from Types.js in the store and I can see it's returned. I'm very much new to react with redux so I'm trying to find my way here and any help is appreciated.
You need to remove the following state definition, since you want to refer to the value in the redux store, not to a local value:
class WidgetData extends Component {
state = {
vesseltypes: ""
};
Then, in your code, you need to refer to the redux store value: this.props.vesseltypes:
class WidgetData extends Component {
state = {
vesseltypes: ""
};
componentWillMount() {
this.props.requestTypes();
}
render() {
return (
<div>
<DropDownList data={this.props.vesseltypes} />
</div>
);
}
}
And you need to change the connect definition:
export default connect(
vesseltypes => state.vesseltypes,
dispatch => bindActionCreators(actionCreators, dispatch)
)(WidgetData);

Map array to get specific value

Im trying to map an array to get a specific value and output it in my PodcastList component.
My json (the red underline is what I want to be viewed in PodcastList.js)
https://itunes.apple.com/se/rss/toppodcasts/limit=100/genre=1314/explicit=true/json
This is my Home component:
import React, { Component } from 'react'
import { fetchPopularPodcasts } from './api'
import PodcastList from './PodcastList'
export default class Home extends Component {
state = {
podcasts: [],
loading: true,
}
async componentDidMount () {
const podcasts = await fetchPopularPodcasts();
this.setState({
podcasts,
loading: false,
})
}
render() {
const { podcasts } = this.state
return (
<div className='container'>
<PodcastList list={podcasts} />
</div>
);
}
}
This is my PodcastList component
import React from 'react'
import { Link } from 'react-router-dom'
import slugify from 'slugify'
const PodcastList = ({ list }) => {
return (
<div className='col-md-12'>
{list.map((pod) => {
return (
<div className='pod-box'>
GET THE LABEL?????
</div>
)
})}
</div>
)
}
export default PodcastList;
This is my Api.js
import Feed from 'feed-to-json-promise'
export async function fetchPopularPodcasts () {
const response = await fetch('https://itunes.apple.com/se/rss/toppodcasts/limit=100/genre=1314/explicit=true/json')
const podcasts = await response.json()
return podcasts.feed.entry
}
export async function fetchPodcast (podId) {
const response = await fetch(`https://itunes.apple.com/lookup?id=${podId}&country=se`)
const podcasts = await response.json()
return podcasts.results
}
export async function fetchPodcastEpisodes (feedUrl) {
const feed = new Feed()
const episodes = await feed.load(feedUrl)
return episodes
}
To get an array containing the label property of each array item object, you can use Array's map method:
let list = [ /* the contents included in your screenshot */ ]
let newList = list.map(pod => {
return pod['im:artist'].attributes.label
})
Note that because of the way the property 'im:artist' is formatted -- i.e., with a colon -- you'll need to access it using bracket notation.
Also, ensure that list is actually an array using the inspector.
Finally, for your case in PodcastList:
const PodcastList = ({ list }) => {
return (
<div className='col-md-12'>
{
list.map(pod => {
return (
<div className='pod-box'>
pod['im:artist'].attributes.label
</div>
)
})
}
</div>
)
}
This assumes all of your React-ness is correct, as in that domain, I'm unfamiliar. (If it's still not working, I would investigate if you need curly braces in your innermost div.)
Also, upon reassessing your code, you'd probably rather use forEach as you're not really trying to create a mapped array; rather, you're trying to gain access to a property for each array element.
Your child component would be set up in the same way as the component
import React from 'react'
import { Link } from 'react-router-dom'
import slugify from 'slugify'
const default class PodcastList extends Component {
_renderList = () => {
let elem = this.props.list && this.props.list.length > 0
(this.props.list.map((pod) => {
<div className='pod-box'>{pod}</div>;
)});
return elem;
}
render() {
return (
<div className='col-md-12'>
{this._renderList()}
</div>
)
}
export default PodcastList;

ReactJs - How to complete onClick before download - href

I have a simple React button component that when clicked should retrieve and download data on the client browser. The problem I am experiencing is that the download is triggered and the csv file downloaded before the data is passed into the href.
Here is my component:
import { Component } from 'react';
import { connect } from 'react-redux';
import { PropTypes } from 'prop-types';
import { ManageUsersSelectors } from 'selectors/Users';
import { BatchRoleActions } from 'actions/Users';
class UsersExportButton extends Component {
constructor() {
super();
this.state = {
users: ''
};
}
getUsers(){
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
this.setState({ users: users});
return this.state.users;
});
}
render() {
return (
<div className="roles-export-button">
<a className="button button-default" href={this.state.users} download={'roles.csv'} onClick={() => this.getUsers()} return true>Export Csv</a>
</div>
);
}
}
function mapStateToProps(state) {
const userIds = ManageUsersSelectors.batchUserIdsSelector(state);
return {
userIds: userIds
};
}
UsersExportButton.propTypes = {
text: PropTypes.string.isRequired,
data: PropTypes.array
};
export default connect(mapStateToProps)(UsersExportButton);
How can I get the getUsers()/onClick function to complete the data retrieval step before downloading?
When i debug my code I can see that the getUsers function returns data - however after the download is triggered
Make sure to bind this to your functions. In your constructor you can do:
constructor() {
super();
this.state = {
users: ''
};
this.getUsers = this.getUsers.bind(this);
}
or you can use the bind this function:
getUsers = () => {
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
this.setState({ users: users});
return this.state.users; // This should be removed, you can use this.state.users throughout this component.
});
}
Why not get the user data in the componentDidMount lifecycle method? It doesn't look like it needs to be called onClick.
{
// ...
componentDidMount() {
this.getUsers();
}
// ...
render() {
return (
<div className="roles-export-button">
<a className="button button-default" href={this.state.users} download={'roles.csv'}>Export Csv</a>
</div>
)
}
}
How about handling the default "link" behaviour manually to get more control? Also you should probably try to access state after setState has been executed via its callback.
e.g.
getUsers(cb){
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
// note the callback of setState which is invoked
// when this.state has been set
this.setState({ users: users }, cb);
});
}
const handleClick = () => {
this.getUsers(() => {
window.open(this.state.whatever)
})
}
<span onClick={handleClick}>Export Csv</span>

Categories

Resources