Ember component is not re-rendering on change of #tracked variable - javascript

My UserTag component is only re-rendering on page refresh. I expect it to refresh when action saveRoles(updatedUserRoles) is used, since it depends on an #tracked variable.
UserRoles is a component from an imported dependency in package.json.
UserTag component:
export default class UserTag extends Component {
constructor() {
super(...arguments);
this.tags = this.updateTags(this.args.tags);
}
#tracked tags;
updateTags(tags) {
if (tags) {
if (!Array.isArray(tags)) tags = Array.of(tags);
return tags.map((tag, index) => this.createTag(tag, index));
}
return tags;
}
...
Defined in my template:
<UserTag #tags={{this.sortedUserRoles}} />
In my controller:
#tracked persistedRoles = null;
#computed('persistedRoles')
get sortedUserRoles() {
console.log('sorting user roles...');
return this.persistedRoles.sort();
}
#action
saveRoles(updatedUserRoles) {
... // Some manipulating of updated roles for AJAX call
this.persistedRoles = [...updatedUserRoles];
}
I've tried adding a console.log in sortedUserRoles() to see if it is being called when I save permissions, but it is not being used. If I add {{this.sortedUserRoles}} outside of the #tags=..., it is called and the sorted roles are shown, which leads me to believe it ignored the tracking when set to a property for a component. Is this the case? (Note: #tags is #tracked in <UserTag>)
Versions:
"ember-cli": 3.22.0
"ember-data": "~3.18.0"

Your problem is that you copy this.args.tags to this.tags in the constructor. This code only runs once.
this.tags = this.updateTags(this.args.tags);
So later when you do
this.persistedRoles = ...
you only update this.args.tags but not this.tags. And because this.args.tags is not used in the template sortedUserRoles is not used either and so ember will have no reason to call the getter.
So the solution is to use this.args.tags in your component directly. If you need to transform the data use a getter:
get tags() {
let tags = this.args.tags;
if (tags) {
if (!Array.isArray(tags)) tags = Array.of(tags);
return tags.map((tag, index) => this.createTag(tag, index));
}
return this.args.tags;
}
Also remove the #computed('persistedRoles').

Related

creating component instances from array.map() and handing down functions

just a quick query.
I have an array of data points and using this to create instances of a component.
The parent component that creates an array of children components also have some functions I wish to hand down to its children. Is there any way this is possible? Check the code and let me know, I am getting errors.
class App extends React.Component {
constructor(props) {
super(props)
this.handleNext = this.handleNext.bind(this)
}
handleNext() {
// some function that uses setState and will cause component to re render
}
render() {
let children = someArray.map(function(elem, index) {
return (
<ChildComponent name = {elem.name} handDownFunc = {this.handleNext}/>
)
})
return (
{children} // I want them to each be able to access and use handleNext
)
}
}
You got this error, because using function keyword instead of arrow function.
For more infromation please follow this link.
So, it will be someArray.map(() => { return() })

Exposing the state of a react widget

I have made a react UI widget thats let's the user select a number of different times and dates. The user's current selection is stored in the state of a top level component, DateTimePicker. I then have a widget wrapper like so:
import ...
export default {
new: (args) => {
const store = {
reactElement: <DateTimePicker
startDate={args.startDate}
endDate={args.endDate}
/>
};
return {
getState: () => {
return store.reactElement.getState(); // DOESN'T WORK
},
render: (selector) => {
ReactDOM.render(store.reactElement, document.querySelector(selector));
}
};
}
};
I want to add a validation to make sure that at least X days/times are selected, but this validation needs to be implemented outside of the widget.
For this, I'll need someway of asking the widget of it 's state. i.e. what has the user selected? Although it seems like the state of the class is not part of the public api of a react component.
How can I acess the state, or is there another way I'm missing?
The solution to doing things imperatively from the parent to the child usually involves getting a ref to the child component. Something along these lines:
export default {
new: (args) => {
let myRef = React.createRef();
const store = {
reactElement: <DateTimePicker
ref={myRef}
startDate={args.startDate}
endDate={args.endDate}
/>
};
return {
getState: () => {
return myRef.current.getState();
},
render: (selector) => {
ReactDOM.render(store.reactElement, document.querySelector(selector));
}
};
}
};
With ref={myRef} added as a prop, whenever DateTimePicker gets mounted, it will assign a reference to the mounted component to myRef.current. You can then use that reference to interact directly with the most recently mounted component.

React setState re-render

First of all, I'm really new into React, so forgive my lack of knowledge about the subject.
As far as I know, when you setState a new value, it renders again the view (or parts of it that needs re-render).
I've got something like this, and I would like to know if it's a good practice or not, how could I solve this kind of issues to improve, etc.
class MyComponent extends Component {
constructor(props) {
super(props)
this.state = {
key: value
}
this.functionRender = this.functionRender.bind(this)
this.changeValue = this.changeValue.bind(this)
}
functionRender = () => {
if(someParams !== null) {
return <AnotherComponent param={this.state.key} />
}
else {
return "<span>Loading</span>"
}
}
changeValue = (newValue) => {
this.setState({
key: newValue
})
}
render() {
return (<div>... {this.functionRender()} ... <span onClick={() => this.changeValue(otherValue)}>Click me</span></div>)
}
}
Another component
class AnotherComponent extends Component {
constructor (props) {
super(props)
}
render () {
return (
if (this.props.param === someOptions) {
return <div>Options 1</div>
} else {
return <div>Options 2</div>
}
)
}
}
The intention of the code is that when I click on the span it will change the key of the state, and then the component <AnotherComponent /> should change because of its parameter.
I assured that when I make the setState, on the callback I throw a console log with the new value, and it's setted correctly, but the AnotherComponent doesn't updates, because depending on the param given it shows one thing or another.
Maybe I need to use some lifecycle of the MyComponent?
Edit
I found that the param that AnotherComponent is receiving it does not changes, it's always the same one.
I would suggest that you'll first test it in the parent using a simple console.log on your changeValue function:
changeValue = (newValue) => {
console.log('newValue before', newValue);
this.setState({
key: newValue
}, ()=> console.log('newValue after', this.state.key))
}
setState can accept a callback that will be invoked after the state actually changed (remember that setState is async).
Since we can't see the entire component it's hard to understand what actually goes on there.
I suspect that the newValue parameter is always the same but i can't be sure.
It seems like you're missing the props in AnotherComponent's constructor. it should be:
constructor (props) {
super(props) // here
}
Try replacing the if statement with:
{this.props.param === someOptions? <div>Options 1</div>: <div>Options 2</div>}
also add this function to see if the new props actually get to the component:
componentWillReceiveProps(newProps){
console.log(newProps);
}
and check for the type of param and someOptions since you're (rightfully) using the === comparison.
First, fat arrow ( => ) autobind methods so you do not need to bind it in the constructor, second re-renders occur if you change the key of the component.
Ref: https://reactjs.org/docs/lists-and-keys.html#keys

Having trouble listing down state map or prop map in React

Here is whats inside my tbody tag:
{this.props.listingData.map((listingData,index) =>
<tr key={index}>
<td>{listingData.location_id}</td>
<td>{listingData.location}</td>
<td>{listingData.description}</td>
</tr>
)}
I'm getting map of undefined error.
Tried logging this.props.listingData & it works.
Here is sample map data which I used:
[{"location_id":1,"location":"HKG","description":"Hong Kong","update_by":null,"update_time":null,"create_by":null,"create_time":null,"rec_state":0},{"location_id":2,"location":"KUL","description":"Kuala Lumpur","update_by":null,"update_time":null,"create_by":null,"create_time":null,"rec_state":0}]
Should be working?
My getDefaultProps:
getDefaultProps() {
return {
//value: 'default value' //called as this.props.value
listingData: [{"location_id":1,"location":"HKG","description":"Hong Kong","update_by":null,"update_time":null,"create_by":null,"create_time":null,"rec_state":0},{"location_id":2,"location":"KUL","description":"Kuala Lumpur","update_by":null,"update_time":null,"create_by":null,"create_time":null,"rec_state":0}]
};
}
Tried this and still getting the error.
You have to define the default props that is a good practice when creating you component, according to the document https://facebook.github.io/react/docs/reusable-components.html#default-prop-values:
var MyComponent = React.createClass({
getDefaultProps: function() {
return {
listingData: []
};
},
render: function () {
//...
}
});
or es2015
class MyComponent extends React.Component {
render() {
//...
}
}
MyComponent.defaultProps = {
listingData: []
}
with static property support
class MyComponent extends React.Component {
static defaultProps = {
listingData: []
};
render() {
//...
}
}
Sounds like you're performing an asynchronous fetch and passing the response as a prop to this component. This means that the first time the component is rendered, it will be undefined (try putting a breakpoint inside render and see for yourself).
As you've seen, you can't call map on undefined, so you need to return something else if you don't have the data yet. Personally I prefer something along these lines:
if (!this.props.listingData) return null;
return this.props.listingData.map(listingData => // ...
There are clearly other ways you can write this. Alternatively, you can provide default props.
Note if location.id is unique, you should use that as your key, rather than the loop index.

Conditional Rendering in Meteor + React

I'm trying to get some conditional rendering in Meter + React. I only want a component to show up if the number of items returned from a collection === 0.
I tried this:
renderInputForm () {
if (Tokens.find().count()) return;
return (<TokenForm />);
}
and put {this.renderInputForm()} in the main render(), but then it flashes for a split second before hiding it…
I know why the flash is happening but trying to avoid it ….
You must wait for the data to finish with synchronization. The flash is there because initially MiniMongo collections are empty. (Also, you might want to avoid Collection.find() in your render function.)
Assuming you use Meteor 1.3.x:
export const MyComponent = createContainer(() => {
let subscription = Meteor.subscribe('something');
if (!subscription.ready()) return {};
return {
tokens: Tokens.find().fetch()
}
}, InternalComponent);
And then check for the existence of props.tokens in your React component.
class InternalComponent extends React.Component {
render() {
if (!this.props.tokens || this.props.tokens.length > 0) return;
return <TokenForm />;
}
}
Find out more about subscriptions here: http://docs.meteor.com/#/full/meteor_subscribe

Categories

Resources