I have a component which is the "base" for another component. I want to add some more functionality to the newly created component
<SomeComponent
onSelect = { this.props.handleSelect }
onDeselect = { this.props.handleDeselect }
selectionList = { valuesList }
value = { values }
origin = "XYZ" />
onSelect triggeres the action this.props.handleSelect
export function handleSelect(value) {
return dispatch => {
dispatch(actionCreator(HANDLE_SELECT, value));
}
}
That actions goes into the reducer
case HANDLE_SELECT: {
const newValues = value_select(state, action);
return {
...state,
find: {
...state.a,
values: newValues
}
}
}
Finally, value_select is called to do all the magic
export const value_select = function(state, action) {
...
const newData = {
XYZ: action.payload
}
return newData
}
How would I mage the "a" from the props from my component acessible in value_select(). I need it where the XYZ is...
Please note that I cannot write anything into the onSelect, hence the onClick event. I am using a predesigned component which I dont want to alter. Only the components that are based on the original one.
You need to add another handler inside SomeComponent and add new argument with prop you want to pass to original handleSelect. If SomeComponent is from vendor and you can't change it's code, than you will have to wrap it
class BetterComponent extends React.Component{
handleSelect = (value) {
this.props.handleSelect(value, this.props.origin)
}
render() {
return (
<SomeComponent
...this.props
onSelect = { this.handleSelect }
/>
)
}
Add new param to your handle select
export function handleSelect(value, origin) {
return dispatch => {
dispatch(actionCreator(HANDLE_SELECT, {
value: value,
origin: origin
}));
}
}
Then origin will be accessible by action.payload.origin inside value_select
Of course now you have to call BetterComponent instead of SomeComponent
<BetterComponent
onSelect = { this.props.handleSelect }
onDeselect = { this.props.handleDeselect }
selectionList = { valuesList }
value = { values }
origin = "XYZ" />
Related
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.
I'm struggling on reorganizing my application written in React.
This is a particular case where I didn't find any specific answer to resolve my issue.
I've been developing my main state's attributes on root component by grouping them by context(not react context) and propagate their contents from node to child nodes like this:
// AppOperator.jsx which is root component
class AppOperator extends Component {
constructor(props) {
super(props);
// first group, attributes and methods regarding cart management
this.CartManager = {
list: [],
methodCM1: this.methodCM1.bind(this),
methodCM2: this.methodCM2.bind(this),
};
// second group, properties related to phase management
this.PhaseManager = {
step: 0,
methodPM: this.methodPM.bind(this),
nextPhase: this.nextPhase.bind(this),
};
this.state = {
CartManager: this.CartManager,
PhaseManager: this.PhaseManager,
}
}
render = () => <View CartManager={this.state.CartManager} {/*...(etc)*/} />
// ...
// CartManager methods
methodCM1 = () => this.setState((state) => {
const { CartManager } = state;
CartManager.list = [];
return state;
});
methodCM2 = () => this.setState((state) => {
const { CartManager } = state;
CartManager.discount = 0.5;
return state;
}, () => this.methodPM());
// ...
// PhaseManager methods
methodPM = () => this.nextPhase();
nextPhase = () => this.setState((state) => {
const { PhaseManager } = state;
PhaseManager.step++;
return state;
});
// ...
// Methods for.... nth object
// ...
}
As you can see there might be methods from different object called inside.
There would not be a problem if you leave their declaration inside the same scope (this instanceOf AppOperator). They would act as 'global' variables. Everyone sees everyone.
But what if it keeps growing and get more complex? How can you maintain such thing?
First thing, I would start using React Context instead of propagating the objects from node to node. No problem with that.
I would then start splitting the objects in different files and call their costructors inside component AppOperator like this:
// AppOperator.jsx
import { CartManager, PhaseManager, } from './Managers';
class AppOperator extends Component {
constructor(props) {
super(props);
// this looks pretty pabolouuus!.
this.state = {
CartManager: new CartManager(),
PhaseManager: new PhaseManager(),
['nth-object']: new NthStateObject(),
}
}
render = () => <View CartManager={this.state.CartManager} {/*...(etc)*/} />
}
and Managers like this:
// CartManager.js
export default class CartManager {
constructor() {
this.list = [];
this.discount = 0;
}
methodCM1 = () => this.setState((state) => {
const { CartManager } = state;
CartManager.list = [];
return state;
});
methodCM2 = () => this.setState((state) => {
const { CartManager } = state;
CartManager.discount = 0.5;
return state;
}, () => this.methodPM());
}
// PhaseManager.js
export default class PhaseManager{
constructor() {
this.step = 0;
}
methodPM = () => this.nextPhase();
nextPhase = () => this.setState((state) => {
const { PhaseManager } = state;
PhaseManager.step++;
return state;
});
}
This would be a cool solution if it could work.
Splitting like so, would leave managers' methods to call setState without knowing AppOperator context.
Instead, binding object's method with AppOperator context would abandon their own properties out and that's also not good:
// PhaseManager.js
export default class PhaseManager{
constructor(that) { // easy not working cheat
this.step = 0;
this.methodPM = this.methodPM.bind(that);
this.nextPhase = this.nextPhase .bind(that);
}
methodPM = () => this.nextPhase();
nextPhase = () => this.setState((state) => {
const { PhaseManager } = state;
PhaseManager.step++;
return state;
});
}
Then, How would you approach this problem? Would you leave it like that, knowing that it might be unmaintanable in future?
Parent component
const myParentComponent = ({ sourceValue, classes, doStuff }) => {
return (
<div>
<MyComponent className={classes.iconWithText} value={sourceValue} onDoStuff={doStuff} />
</div>
);
}
const mapStateToProps = () => {
...
}
function mapDispatchToProps(dispatch) {
return {
doStuff: () => dispatch(doStuffAction())
};
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(myParentComponent));
uses
Child component
export default class MyComponent extends Component {
handleClick = () => {
console.info(this.props.value);
this.props.onDoStuff(this.props.value);
}
render() {
return (
<SomeIcon className={this.props.className} onClick={this.handleClick} />
);
}
}
Ultimately, the action being called itself does nothing more than log:
export function doStuff(value) {
console.info('At doStuff action', value);
return { type: DO_STUFF, value };
}
Technically, everything seems to work, there's no errors. But, that last console.info in the action logs value as undefined. I'm pretty sure it's because I map the onClick action like this:
function mapDispatchToProps(dispatch) {
return {
doStuff: () => dispatch(doStuffAction())
};
}
If I provide a paramater like so:
function mapDispatchToProps(dispatch) {
return {
doStuff: () => dispatch(doStuffAction('123456'))
};
}
Then the doStuff action logs that 123456 value.
It does seem that the sourceValue being passed to onDoStuff from the main component does not have any effect.
What do I need to do to make sure the original sourceValue is passed?
All you need to do is accept a param in the arrow function and pass it on to the action like
function mapDispatchToProps(dispatch) {
return {
doStuff: (value) => dispatch(doStuffAction(value))
};
}
In case you have to pass multiple params, you can write it like
function mapDispatchToProps(dispatch) {
return {
doStuff: (value, key) => dispatch(doStuffAction(value, key))
};
}
If you have multiple parameters that varies upon usage, say from one component you want to pass value to doStuffAction and in some component you want to pass value and key then its preferred to make the value as an object like
function mapDispatchToProps(dispatch) {
return {
doStuff: (obj) => dispatch(doStuffAction(obj))
};
}
and action:
export function doStuff({value, key}) {
console.info('At doStuff action', value, key);
return { type: DO_STUFF, value };
}
I'm trying to abstract away my onClick function(s) into its own file and then later use it in a component's button(s).
However the function needs to conditionally update the state of the component it is being used in.
export let ButtonClickHandlers = {
refreshDatabase: (url: string) => {
service.fetchJson<any>(url)
.then(res => {
if(res.isSuccessStatusCode){
//update state
}
else{
//update state
}
});
},
//more functions to be added later
}
In my component I want to do something like this:
import {ButtonClickHandlers} from '../ButtonClickHandlers';
<button onClick = {ButtonClickHandlers.refreshDatabase} />
How do I implement this functionality?
This is how i have done it, there might be better ways.I have imported the clickevent and passed the context as paremeter
clickevenhandler.js
const clickEvent = (context) => {
context.setState({ click: true });
}
export default clickEvent;
my test component
import clickEvent from './clickhandler';
class TestComp extends React.Component<any, any>{
constructor(props) {
super(props);
this.state = {
click: false
}
}
renderclicked() {
if (this.state.click) {
return (<div>clicked</div>);
}
else {
return (<div></div>);
}
}
render() {
return (<div>Test
<button onClick={() => clickEvent(this)}>click</button>
{this.renderclicked()}
</div>);
}
}
I have a Connector, which has mapDispatchToProps and mapStateToProps functions, and I need to dispatch a action from my main component.
I'm getting an error saying dispatch is not defined when I'm trying to dispatch fetchPlaces(this.props.user.id)
this.props.user.id has value 1.
I need to get the user id and pass it to fetchPlaces, which intern gets me the places of the user. I'm not sure how to do it.
Connector
const mapStateToProps = function (store) {
return {
elements: store.elements.elements,
places: store.places.places,
geocode : store.geocode.geocode,
user : store.user.user
};
};
const mapDispatchToProps = function (dispatch) {
return {
userAction : dispatch(fetchUser()),
elementsAction : dispatch(fetchCategories()),
placesAction: (id) => { dispatch(fetchPlaces(id)) }
}
}
class BasicinfoConnector extends React.Component{
render() {
console.log(this.props.user);
if(typeof this.props.user.id != "undefined"){
return (
<Basicinfo elements={this.props.elements} places={this.props.places} geocode={this.props.geocode} user={this.props.user}/>
);
}
else{
return null;
}
}
}
export default Redux.connect(mapStateToProps, mapDispatchToProps)(BasicinfoConnector);
Component :
componentWillMount() {
console.log(this.props);
console.log(this.props.user.id);
dispatch(fetchPlaces(this.props.user.id))
}
Is placesAction: (id) => { dispatch(fetchPlaces(id)) } the right syntax of doing it?
UPDATE
I changed componentWillMount :
componentWillMount() {
console.log(this.props);
console.log(this.props.user.id);
dispatch(this.props.placesAction(this.props.user.id))
}
and mapDispatchToProps :
const mapDispatchToProps = function (dispatch) {
return {
userAction: bindActionCreators(fetchUser, dispatch),
elementsAction: bindActionCreators(fetchUser, dispatch),
placesAction: (id) => { dispatch(fetchPlaces(id)) }
}
}
Still have the same error.
You need to pass the property down to the next level, either by sharing all your props like this:
<Basicinfo {...this.props} />
or only the particular ones that you want
<Basicinfo placesAction={(id) => this.props.placesAction(id)} />