How can i re-render observable array-data in Mobx? - javascript

How can i re-render observable array-data in Mobx?
I used observer decorator in this class.
interface IQuiz {
quizProg: TypeQuizProg;
qidx: number;
state: IStateCtx;
actions: IActionsCtx;
}
#observer
class Comp extends React.Component<IQuiz> {
private _qtype: common.TypeQuiz = '';
private _qtime = 60;
#observable _quizs: common.IQuizData[] = [];
constructor(props: IQuiz) {
super(props);
makeObservable(this);
};
public componentWillMount() {
this._quizs = this.props.actions.getData();
}
#action
public quizSelect(idx: number, v: boolean) {
this._quizs[idx].selected = true;
this._quizs.slice();
}
public render() {
const {state, actions, qidx} = this.props;
let ItemComponent;
if(this._qtype === 'unscramble') ItemComponent = QuizUnscramble;
if(this.props.state.prog !== 'quiz') {
return <QuizList
state={state}
quizs={this._quizs}
quizSelect={(idx: number, v: boolean) => {
this.quizSelect(idx, v);
}}
/>
} else {
return (
<QuizBox
view={true}
className="t_quiz"
quizProg={state.quizProg}
qidx={qidx}
qtype={this._qtype}
qtime={this._qtime}
quizs={this._quizs}
ItemComponent={ItemComponent}
isteacher={false}
setQuizProg={actions.setQuizProg}
/>
);
}
}
}
const Quiz = useTeacher((store: TeacherContext) => (
<Observer>{() => (
<Comp
quizProg={store.state.quizProg}
qidx={store.state.qidx}
state={store.state}
actions={store.actions}
/>
)}</Observer>
));
I use mobx6 and react-typeacript.
Comp class dosen't re-rendering when i run quizSelect function.
Do not check mobx observable data point of different deeply?
#observable.deep is not working too.
How can i fix it?

I believe the problem lies in the componentWillMount method where you're overwriting the original array which you've marked observable. Things like arrays and objects are handled a bit differently since they are reference types and not value types.
Pushing the elements into the existing array rather than reassignment should fix the issue:
public componentWillMount() {
this._quizs.push(...this.props.actions.getData());
}

I found answer.
Reference type need reallocation.
this._quizs[idx].selected = v;
this._quizs = _.cloneDeep(this._quizs);

Related

How to render React (with Redux) component only when props are received?

Let us assume we have a statefull React component (configured to work with Redux):
export class SomeComponent extends Component {
state = {
someObject: {}
};
componentWillMount() {
this.props.getNews();
this.props.getFakeNews();
}
render() {
const {
news,
fakeNews
} = this.props;
if(_.isEmpty(news) || _.isEmpty(fakeNews)){
return <div>Loading</div>
}else{
return <div>Here all component stuff</div>
}
}
SomeComponent.propTypes = {
news: PropTypes.array.isRequired,
fakeNews: PropTypes.array.isRequired
};
export const Some = connect(
state => ({
news: newsSelectors.list(state),
fakeNews: fakeNewsSelectors.list(state)
}),
{
getNews,
getFakeNEws
}
)(withStyles(styles)(SomeComponent), withRouter(SomeComponent));
This component will re-render two times during getting news and fake news. In the render method we need to check if both of them are loaded.
Is there any way to trigger render only when all props are loaded?
In a perfect scenario I'd like to have no detailed null/empty check on the set of props. I believe React or Redux should perform this operation on its own as long the prop is configured as required.
You can add a lifecycle method `shouldComponentUpdate(nextProps, nextState).
You can add the following method and it should resolve it for you:
shouldComponentUpdate(nextProps, nextState) {
if (_.isEmpty(nextProps.news) || _.isEmpty(nextProps.fakeNews)) {
return false;
}
return true;
}
You could do something like:
// HOC factory
function ifComponent (predicate, PlaceHolder) {
return Component => class If extends React.Component {
render () {
if (predicate(this.props)) {
return <Component {...this.props} />
}
return <PlaceHolder {...this.props} />
}
}
}
}
// create the customHOC
const whenPropsLoaded = ifComponent(props => props.news && props.fakeNews, Loader);
// compose the two HOCs using the `compose` function in redux (simple function composition)
const News = compose(
connect(getNewsProps),
whenPropsLoaded(DisplayNews)
);
As a side note you may be interested in the recompose utility library bad its branch HOC (docs here). I think this is pretty much what you want as you seem to know about HOCs.
If you want to avoid null and undefined values from redux. You can use Selectors it was very easy to avoid those things.
const newsSelectors = (state) => {
if(!state.list) { *//list is null or undefined*
return [] or {} *//your wish (Proptypes required value)*
}
else {
return state.list
}
}
export { newsSelectors };
I think you can solve the issue if you rewrite the render function as below.
render() {
const {
news,
fakeNews
} = this.props;
return (
{news && fakeNews ?
<div>Here all component stuff</div>
: <div>Loading</div> }
)
}
I hope this helps you.

Refactoring UNSAFE_componentWillReceiveProps

I have an IFrameComponent component, inspired by this post.
It looks basically like this :
class IFrameComponent extends React.Component {
shouldComponentUpdate() {
return false;
}
componentWillReceiveProps(nextProps) {
if(this.props.content !== nextProps.content) {
const html = getHTMLFromContent();
const fdoc = this.iFrame.contentDocument;
fdoc.write(html);
}
}
render() {
return (<iframe sandbox="..." ref={f => this.iFrame = f} />);
}
}
Now that componentWillReceiveProps is considered unsafe, I'm trying to get rid of it.
The ways React advices to refactor componentWillReceiveProps are basically either to use static getDerivedStateFromProps or componentDidUpdate
Sadly, componentDidUpdate will never get called, because shouldComponentUpdate returns false (and I think this is fine ?) and I wouldn't be able to access this.iFrame reference in the static method getDerivedStateFromProps.
How would one refactor this code ?
I think, One possible way is:
let iFrameRefs = {}
class IFrameComponent extends React.Component {
static getDerivedStateFromProps (props) {
if (iFrameRefs[props.id]) {
const html = getHTMLFromContent();
const fdoc = iFrameRefs[props.id].contentDocument;
fdoc.write(html);
}
return null
}
shouldComponentUpdate() {
return false;
}
render() {
return (<iframe sandbox="..." ref={f => iFrameRefs[this.props.id] = f} />);
}
}
Now from Parent Component pass unique id to each component. You can also manage id in IFrameComponent.
<IFrameComponent id='1' />
<IFrameComponent id='2' />

How to setState a class object in React?

I have an array of objects inside my class that I am modifying and only when a keypress happens do I want to render this object visually.
class Example extends React.Component {
constructor(props) {
super(props);
this.myArr = []; // this is an array of objects
}
render() {
return (
???
);
}
}
Now I modify the contents of this.myArr in many different methods. And only when I'm ready (on a keypress or some other event) do I want to render it.
Now in my render() should I have a reference to this.myArr and then use this.forceUpdate() when I want to force a re-render.
Or should I move myArr into this.state.myArr, and modify this.state.myArr in my methods and when I am ready to display it, in my render() reference to this.state.myArr, and somehow force a rerender with this.setState(myArr: this.state.myArr);
***Second Update - I think this may be what you want. Obviously, you'll need to add a lot of logic for your mouse click events. It should point you in the right direction though.
class Example extends React.Component {
constructor(props) {
super(props);
this.myArr = [];
this.state = {
myArr: [{ width: 10 }, { width: 20 }], // header widths
};
}
// call changeHeaders when needed
// it will update state, which will cause a re-render
changeHeaders = (column, newWidth) => {
const newArr = [...this.state.myArr];
if (newArr[column]) {
newArr[column].width = newWidth;
}
this.setState({ myArr: newArr });
}
renderArray = () => {
return this.state.myArr.map(({ width }) => <div>{width}</div>);
}
render() {
return (
<div>
{this.renderArray()}
</div>
);
}
}
Either way would work but I think its better practice to use this.state to hold your array and use this.setState() to force a re-render and call the this.setState within your keypress event callback
Here is how you update your array value -
Correct modification of state arrays in ReactJS
There's a very good explanation why you should avoid using this.forceUpdate() in here answered by Chris.
In this case you should use state. State is intended to be used for any data that affect how your component looks. Remember that you may not modify your state directly, it's an antipattern. Instead, you should create a copy of the array and update the state with that modified copy. Like so:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {myArr: []}; // this is an array of objects
}
mutateArraySomehow() {
const nextArr = [...this.state.myArr];
nextArr.push('heyyoooo');
this.setState({myArr: nextArr});
}
render() {
return (
???
);
}
}
This is how i would have done it
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
myArr: [],
display: false
}
}
render() {
if(this.state.display) {
return (
<MyComponent onKeyPress=(ev=>{
this.setState({display: true})
})
/>
);
} else {
return (<div></div>)
}
}
}
When there is a modification to the array elements, you need to do a setState of status to true.This would perform the conditional rendering and will display the modified array.
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
myArr = []; // this is an array of objects
status:false
}
}
modifyArr = () => {
//some array modifications
this.setState({status:true})
}
render() {
return (
{this.state.status ? null : (<div>`Display your array here`</div>)}
);
}
}
In short, define state inside class like:
state: {
firstName: String
} = {
firstName: ''
}
And inside render function you would do this:
this.setState({ firstName: 'Junior' })

Is there a non-hacky way to force React to destroy and re-construct an element?

Long story short, I would like React to create a completely new element rather than reuse an existing element. This is so it runs the constructor again.
By default, React tries to reuse as much as possible. A hacky workaround is to add a key property to an element. For example:
class Foo extends React.Component {
constructor() {
super();
this.state = {
forceReloadCounter: 0,
};
}
_forceReload() {
this.setState({forceReloadCounter: this.state.forceReloadCounter + 1});
}
render() {
return (
<ChildElement key={this.state.forceReloadCounter}/>
);
}
}
If _forceReload runs, it changes ChildElement's key, so React destroys the old instance of ChildElement and constructs a new one. However, this solution is obviously very hacky. It would be confusing to anyone else reading the code. Is there a better solution?
I wrote a class that uses my hacky solution in a cleaner way.
class RecreateChildOnPropsChange extends React.Component {
constructor() {
super();
this._forceRecreateCounter = 0;
}
shouldComponentUpdate(nextProps) {
assert(nextProps.children, 'Every props object should have childrens.');
let props = this.props;
if (props === nextProps) {
return true;
}
let keys = Object.keys(props);
let nextKeys = Object.keys(nextProps);
if (keys.length !== nextKeys.length) {
return false;
}
for (let key of keys) {
if (key !== 'children' && (!nextProps.hasOwnProperty(key) || props[key] !== nextProps[key])) {
return true;
}
}
return false;
}
render() {
this._forceRecreateCounter++;
return React.cloneElement(
React.Children.only(this.props.children),
{key: this._forceRecreateCounter}
);
}
}
It would be used like this:
<RecreateChildOnPropsChange startingPage={page}>
<Newsfeed startingPage={page}/>
</RecreateChildOnPropsChange>
If any property of RecreateChildOnPropsChange changes, it destroys and recreates its child element.

React Native: renderRow not executed

In the following code, when setState is called from campaignsUpdated, render gets logged to the console, but not renderRow:
var React = require('react-native'),
Bus = require('../Bus'),
styles = require('../Styles'),
CampaignsStore = require('../stores/Campaigns'),
CampaignItem = require('./CampaignItem'),
{
Component,
Text,
TextInput,
ListView,
View,
NavigatorIOS,
ActivityIndicatorIOS
} = React
class CampaignList extends Component {
constructor(props) {
super(props)
this.state = {
dataSource: new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2})
}
}
componentDidMount() {
this.addListeners()
Bus.emit('campaigns:search', '')
}
componentWillUnmount() {
this.removeListeners()
}
render() {
console.log('render')
return (
<View style={styles.container}>
<TextInput
style={styles.searchInput}
placeholder='Campaign Name'
value={this.state.campaignName}
onChange={this.campaignSearchChanged.bind(this)}/>
<ListView
dataSource = {this.state.dataSource}
renderRow = {this.renderRow.bind(this)}/>
</View>
)
}
renderRow(campaign) {
console.log('renderRow')
return <CampaignItem campaign={campaign}/>
}
addListeners() {
Bus.on({
'campaigns:updated': this.campaignsUpdated.bind(this)
})
}
removeListeners() {
Bus.off({
'campaigns:updated': this.campaignsUpdated.bind(this)
})
}
campaignsUpdated(event) {
var campaigns = event.data
this.setState({
dataSource: this.state.dataSource.cloneWithRows(campaigns)
})
}
campaignSearchChanged(event) {
var campaignName = event.nativeEvent.text
Bus.emit('campaigns:search', campaignName)
}
}
module.exports = CampaignList
What am I doing wrong here?
You are passing ListView a function renderRow that returns a component. You would have to call that function within ListView once it is passed, presumably during a map over campaigns.
By the looks of it the most likely case is that you have a classic React mutability issue here.
I.e. I suspect your 'campaignsUpdated' method is called with either the same Array instance it received last time, or the elements within the list are the same instances.
Try using:
campaignsUpdated(event) {
var campaigns = event.data.slice(); // <-- clone the array
this.setState({
dataSource: this.state.dataSource.cloneWithRows(campaigns)
})
}
If that doesn't work, then you either make the part that manages your list of compaigns create new copies when changes are made (e.g. const clone = {...campaign, title:"A new Title"}) or update your rowHasChanged method to see if the title (or whatever data you need) has actually changed.
Here are two really good videos about immutability in JavaScript here:
https://egghead.io/lessons/javascript-redux-avoiding-array-mutations-with-concat-slice-and-spread
https://egghead.io/lessons/javascript-redux-avoiding-object-mutations-with-object-assign-and-spread

Categories

Resources