How to use a callback in a render() of React component - reactjs

I want to use the result of a server method that does a count on a collection to show it in the render method of a react component. I understand that I must do it from the callback function but it doesn't work for me.
Server Method:
export const analysis = new ValidatedMethod({
name: "analysis",
validate: new SimpleSchema({
codigo: {
type: String
},
rta: {
type: String
}
}).validator(),
run(one) {
const rta = Respuesta.find({
codigo: one.codigo,
rtatexto: one.rta,
activo: true
}).count();
console.log(rta);
return Number(rta);
}
});
Call from client:
export default class AnalysisFila extends Component {
constructor(props) {
super(props);
}
render() {
const one = { codigo: this.props.codigo, rta: this.props.opcion };
const rta = analysis.call(one, (err, res) => {
return (
<Table.Row>
<Table.Cell> {this.props.opcion} </Table.Cell>
<Table.Cell> {res} </Table.Cell>
</Table.Row>
);
});
}
}
How do I use the value of res in my component's render method?

Asynchronous Functions
Before I answer the question, it is important to understand the difference between synchronous and asynchronous functions in JavaScript.
Therefore, if you are new to JavaScript or asynchronous behaviours, I recommend you read up on this excellent answer and explanation about the topic
Explanation for the Problem
The problem here is that React's render() method is a synchronous method, meaning React expects a synchronous return value with jsx (or a React.Element for that matter).
For React, your current render function actually returns nothing or undefined, as you do not a have a synchronous return statement. And so nothing will get rendered.
React has no way of knowing when your callback is called or what it returns, as it is executed completely out of Reacts context.
Solution to the Problem
The way to go here is to use Reacts state which is exactly for these kind of scenarios.
By using state and the method setState(), you can asynchrounously trigger the render-method to be called again as soon as the response from your API delivered the data. Also, with this.state, you can bring the response-data into the react-context and let it know about it.
A full example could look like this:
export default class AnalysisFila extends Component {
constructor(props) {
super(props);
this.state = {
loaded: false
opcion: {}
};
}
componentDidMount() {
const one = { codigo: this.props.codigo, rta: this.props.opcion };
const rta = analysis.call(one, (err, res) => {
this.setState({
loaded: true,
opcion: res
}
});
}
render() {
const content = this.state.loaded ? this.state.opcion : 'Loading...';
return (
<Table.Row>
<Table.Cell> {this.props.opcion} </Table.Cell>
<Table.Cell> {content} </Table.Cell>
</Table.Row>
);
}
}
I also used the componentDidMount-Lifecycle-Method of React, as it is the recommended way to trigger API Calls. You can read more about Lifecycles in the official lifecycle documentation
The Life-Cycle of this Component will be like the following:
Component will get created and constructor will set an initial value for this.state
render() method is called with this.state.loading === false - thus the content will default to the string 'loading...'
The componentDidMount() function gets called and will trigger your API-Call
Your callback is called and executes setState(), putting the response-data into this.state
Now that react knows the state was changed, it calls the render() method again. But this time, this.state.loading === true and there is some value for this.state.opcion - thus the content will be the value from your response.

Related

Render Props - need to execute function after some component configuration

UPDATE, SOLUTION FOR NOW:
I moved scopes to the state and now the scopes data is up to date.
I am using render prop with the new context API. To make it easier, lets say that API got two methods. Method A is used by ChildComponent via Context API, and methodB is used as render prop.
The problem is that I need on init below order:
ChildComponent runs methodA from context API
Component property: this.scopes is populated
When methodB runs (from render props), it knows about this.scope
For now, methodB runs before the this.scope is populated (this.scope = {}) by methodA
I tried with setTimeout, but I don't think it is the best idea...
class Component extends React.Component{
scopes = {};
render(){
const api = {
methodA: (name, fields) => {
this.scopes[name] = fields;
},
methodB: (name) => {
console.log(this.scopes[name])
}
}
return (
<ComponentContext.Provider value={{ api }}>
{typeof children === 'function' ? children(api) : children}
</ComponentContext.Provider>
);
}
}
/************* CHILD COMPONENT */
class ChildComponent extends React.Component{
static contextType = ComponentContext;
componentWillMount() {
const { api } = this.context;
api.methodA(name, this.fields);
}
render() {
const { children } = this.props;
return children;
}
}
/************* COMPONENTS IMPELENTATION WITH THE PROBLEM */
<Component>
{(api) => (
<ChildComponent name="foo"> // it should at first add foo to the Component this.scope;
<div>Some Child 1</div>
</ChildComponent>
<ChildComponent name="bar"> // it should at first add bar to the Component this.scope;
<div>Some Child 2</div>
</ChildComponent>
{api.methodB(foo)} // not working because for now this.scopes is empty object (should be: {foo: someFields, bar: someFields})
)}
</Component>
I expect to result this.scope = {foo: ...someFields, bar: ...someFields }, for now this.scope= {} after initial run, next invocation of methodB works okey, and (this.scope = {foo: ...someFields, bar: ...someFields}.
Thank you for any tips.
You add and use the scope in the same lifecycle, thus using the old version of passed context. You can move your api.methodB(foo) from Render() method to componentDidUpdate() step, which will ensure you have a new context when it executes.
In case the initialization occurs only once and synchronously, parent Component can be considered initialized when it is mounted.
If the purpose of methodB is to return data that a parent was initialized with, parent's state should be updated on initialization. It's an antipattern to store component's state outside this.state. There may be a flag that definitely indicates that the initialization was completed:
class Component extends React.Component{
state = {
scopes: {},
init: false,
methodA: (name, fields) => {
if (this.state.init) return;
this.state.scopes[name] = fields;
},
methodB: (name) => this.state.init ? this.scopes[name] : null
};
componentDidMount() {
this.setState({ init: true });
}
render(){
return (
<ComponentContext.Provider value={this.state}>
...
</ComponentContext.Provider>
);
}
}
This way api.methodB(foo) will return null, unless the initialization is completed.

React: update state with props change - handleChange and getDerivedStateFromProps

I'm trying to make some component which data output depends on some external API.
So I have this snippet:
class Parent extends Component {
constructor(props) {
super(props)
this.state = {
somethingFromAPI: ''
}
}
componentDidMount() {
/*
something on axios.get() which updates this.state.somethingFromAPI
which normally can have some time delay till executed
*/
}
render() {
return (
<Child value={this.state.somethingFromAPI} />
)
}
}
class Child extends Component {
constructor(props) {
super(props)
this.state = {
value: this.props.value || ''
}
}
handleChange(event) {
this.setState({
value: event.target.value
})
}
static getDerivedStateFromProps(props, state) {
// if difference
return {
value: props.value
}
}
render() {
return (
<div>
<input value={this.state.value} onChange={this.handleChange.bind(this)} />
</div>
)
}
}
ReactDOM.render(
<Parent />
document.getElementById('app')
);
Seems like this works fine, initializing component, and getting API data, after that, input value seems to be updated, which is what I expect.
Problem that hurts me a lot is if I type something inside input, that will call handleChange, but will also trigger this getDerivedStateFromProps and will replace newer inputed value with that "old" from API.
Is this good way of doing this, maybe I made mistake at start with understanding of how it should be done? Guide me in right direction.
I'm yet pretty new to React.
Generally, need to make form which I can use for new input, or updating existing data (like some posts, etc.), so I can load API data.
Best regards.
Did you consider using shouldComponentUpdate instead if using getDerivedStateFromProps
something like this may solve your problem:
shouldComponentUpdate(nextProps, nextState) {
const { value: nextPropsValue } = nextProps;
const { value: propsValue } = this.props;
const { value } = this.state;
if (nextPropsValue !== propsValue && nextPropsValue !== value) {
this.setState({
value: nextPropsValue
});
}
return value !== nextState.value;
}
Update the answer adding comparison with current props value
I think using getDerivedStateFromProps here may be unnecessary. If you want to prevent a render in certain cases, consider using shouldComponentUpdate https://reactjs.org/docs/react-component.html#shouldcomponentupdate. But it sounds like you basically just need to use your input change handler to keep the state of the input, which you're already doing.
You should also check this article out on why someone shouldn't use getDerivedStateFromProps. It's very informative.

Using ImmutableJS and React setState

I'm currently experiencing problems with getting ImmutableJS and React to work properly. This is not using Redux, therefore I am updating state within the component.
The problem is after I update state, my getter properties are missing on the next rerender causing an Error. Why does setState strip these methods away?
Am I not using ImmutableJS correctly? Is ImmutableJS only intended to be with Redux?
const AssociateRoleLocationStateFactory = RecordFactory<AssociateRoleLocationState>({
isEditing: false
})
export default class IAssociateRoleLocation extends React.Component {
constructor(props) {
super(props)
this.state = new AssociateRoleLocationStateFactory({
isEditing: false
})
// `this.state` has `.get` method to retrieve properties
console.log(`initial state:`, this.state) // Record {_map: Map}
}
toggleEditing() {
let nextState = this.state.set('isEditing', !this.state.get('isEditing'))
// `nextState` has `.get` method to retrieve properties
console.log(`next state:`, nextState) // Record {_map: Map}
this.setState(nextState)
}
render() {
console.log(this.state) // {_map: Map} // Its missing `.get` method on second render
let isEditing = this.state.get('isEditing')
return (
<div className="site-single-column">
{isEditing ? (
<div>
Is Editing!!
</div>
) : (
<button style={{ alignSelf: 'center' }} onClick={this.toggleEditing}>
Toggle
</button>
)}
</div>
)
}
}
Currently, React only supports plain js object as the state. You can wrap whatever you want in another key/value layer like
constructor(props) {
super(props);
this.state = {
payload: new AssociateRoleLocationStateFactory({
isEditing: false
})
}
}
There are discussions around this going on like this: https://github.com/facebook/react/issues/3303
But before that, stay pure.

Unable to access nested data

I noticed I cannot access to a nested js object.
this.DB.get(2) returns an object in the form
{
id:1,
name: "my title",
Obj:{en:"english",fr:"french"}
}
This is the component
export default class Item extends Component {
state = {
Item:{}
}
constructor(props) {
super(props);
}
componentDidMount(){
this.DB = $DB()
this.setState({
Item: this.DB.get(2)
})
}
render() {
const { params } = this.props.navigation.state;
const id = params ? params.id : null;
const Item = this.state.Item
return (
<View style={{flex:1}}>
Number field: {Item.id} |
String field: {Item.name} |
Object field: <FlatList data={Object.entries(Item.Obj)} />
</View>
);
}
}
The problem: I cannot access to Item.Obj.
I got it, setState is async, I'm not sure this is causing the issue though. Anyway: is there a clean way for render the component in setState callback?
edit: I've just rebuilt (still in debug mode) and now it works, I do not changed anything, just added some console.log() around.
Anyway, I don't feel so safe. setState() is async and still I see around the web this function used wildly as it would be sync.
If the data I want to render are in the state, is there a way to render the component always after the state update?
Another doubt: you see the Component, it just does an access to a big JS object and shows some data and that's all. Do I really need to pass through the component state?
What do you think if I would move those 2 lines inside the render() method?
const DB = $DB()
const Item = DB.get(2)

Where do I call setState for redux values?

I'm pretty new to react native and async programming, and trying to understand how to "sync" redux state values and local state values.
For example, I have a text field "aboutMe" stored server side, and using mapStateToProps to place it into props:
const mapStateToProps = (state) => {
return { aboutMe: state.aboutMe };
}
In render, I have a TextInput I'm using so that the user can edit this field, and I would like to default to what is saved on the server side:
<TextInput
onChangeText={(aboutMe) => {
this.setState({aboutMe});
}}
value={this.state.aboutMe}
/>
Basically, somewhere I need to call
this.setState({ aboutMe: this.props.aboutMe });
Where is the right place to this? I was trying to use componentWillReceiveProps, but that lifecycle method is not called on constructor, so I would need to setState twice (in constructor and in componentWillReceiveProps).
Is there another way to do this? I feel like this is a pretty generic problem that a lot of react native developers have solved but I couldn't find a generally accepted way online.
Thanks!
Edit:
I have alot of TextInputs, so I have a separate button to call the action to save the variables:
<Button onPress={()=>{
this.props.saveUserInput(this.state.aboutMe,
this.state.name, this.state.address, ....}}>
<Text> Save changes </Text>
</Button>
From the comments, I understand that it's possible to call the save action onChangeText... but is that too much traffic back and forth? Would it be better to save all of the variables locally to state and then call a save for everything at once? Also, what if the user would like to "cancel" instead of save? The changes would have been already saved and we will not be able to discard changes?
1) If your component is a controlled component (you need state in it) and the request is asynchronous indeed you have to set the state in the componentWillReceiveProps like this:
class ExampleComp extends Component {
constructor(props) {
super(props);
this.state = {
aboutMe: ""
}
}
componentWillReceiveProps(nextProps) {
this.setState({
aboutMe: nextProps.aboutMe,
});
}
render() {
return (
<TextInput
onChangeText={(aboutMe) => {
this.setState({aboutMe});
}}
value={this.state.aboutMe}
/>
);
}
}
Keep in mind the key here is that the state must remain the single source of truth from now on.
2) The other option would be, you can wait until the request is finished in the parent component and then set the aboutMe in your constructor, this way you can avoid componentWillReceiveProps. For example:
class ParentComp extends Component {
render() {
return (
<div>
{this.props.aboutMe && <ExampleComp/>}
</div>
);
}
}
class ExampleComp extends Component {
constructor(props) {
super(props);
this.state = {
aboutMe: props.aboutMe
}
}
render() {
return (
<TextInput
onChangeText={(aboutMe) => {
this.setState({aboutMe});
}}
value={this.state.aboutMe}
/>
);
}
}
The downside of this is that the text input won't be shown until the request is finished.
Since you have edited your question, it is more clear what you want to achieve, so I want to address that.
You could keep the state of your controlled input elements in the component, then use the redux store to store persistent data and to populate the default values.
class Component extends React.Component {
constructor(props) {
super(props)
this.state = {
aboutMe: props.aboutMe,
... // other data
}
}
handleSubmit = (event) => {
event.preventDefault() // To prevent redirect
// Dispatch the save user input action
this.props.dispatch(saveUserInput(this.state))
}
render() {
return (
<form onSubmit={this.handleSubmit} />
<TextInput onTextChange={text => this.setState({...this.state, aboutMe: text}) />
... // input fields for other data
// Clicking this fill trigger the submit event for the form
<button type="submit">Save</button>
</form>
)
}
}

Resources