Store auto-updating object inside React state - javascript

I have this situation but I don't know if it's ok to do something like this.
If I save an object inside the state of my component, can the object modify itself without using setState?
File A.js
export default class A {
constructor(value) {
this.value = value;
}
setValue(value) {
this.value = value;
}
}
File B_Component.js
import React from "react";
import A from "./A";
class B_Component extends React.Component {
constructor(props) {
super(props);
this.state = {
aObj = new A("foo")
}
}
bar = () => {
this.state.aObj.setValue("bar");
}
...render etc...
}
Basically this should modify the state of the component without using setState.
Is this correct or there may be problems?

Is this correct or there may be problems?
There may be problems. :-) If you're rendering that object's value, then you're breaking one of the fundamental rules of React, which is that you can't directly modify state. If you do, the component won't re-render.
Instead, you create a new object and save the new object in state:
bar = () => {
this.setState({aObj: new A("bar")});
}
Or if you want to reuse other aspects of the object and only change value:
bar = () => {
const aObj = new A(this.state.aObj); // Where this constructor copies all the
// relevant properties
aObj.setValue("bar");
this.setState({aObj});
}
The A constructor would be
constructor(other) {
this.value = other.value;
// ...repeat for any other properties...
}
Or you might give A a new function, withValue, that creates a new A instance with an updated value:
class A {
constructor(value) {
this.value = value;
// ...other stuff...
}
withValue(value) {
const a = new A(value);
// ...copy any other stuff to `a`...
return a;
}
}
Then:
bar = () => {
this.setState({aObj: this.state.aObj.withValue("bar")});
}

Related

How do I mock "this" when writing a unit test for a React module?

I'm writing a React app where the App.js has a lot of logic inside it tying together the rest of the components and keeping a coherent state. I have moved some of the code that were originally in App.js to helpermodule.js and bind the imported functions so that they can manipulate the state of the App component.
This is my setup:
App.js
import helperFunction from './helpermodule'
class App extends Component {
constructor(props) {
super(props)
this.state = { foo: 'bar' }
this.helperFunction = helperFunction.bind(this)
}
}
helpermodule.js
function helperFunction() {
this.setState({
bar: 'calculated' + this.state.foo,
})
}
export { helperFunction }
And I want to write a unit test for the function(s) inside the helpermodule. I can't find the relevant parts in the Enzyme or Jest documentation where this is covered. The goal is to see what effect helperFunction has on the App state.
I have tried things like this
test('helperFunction', () => {
let state = { foo: 'bar' }
let setState = (obj) => { this.state = obj }
return expect(helperFunction().bind(this))
.toBe({
...newShift,
id: 0,
})
}
But that just returns an error; TypeError: Cannot read property 'state' of undefined. Perhaps my entire approach is wrong so the problem can be circumvented but I'm not sure.
Don't use this.setState in an external function
helpermodule.js
function helperFunction(stateFoo) {
return {bar: 'calculated' + stateFoo};
}
export { helperFunction }
Then use the result to set state in the component. Also don't setState in constructor.
import helperFunction from './helpermodule'
class App extends Component {
constructor(props) {
super(props)
this.state = { foo: 'bar', ...helperFunction('bar') }
}
}
This will make testing easier as well as being a better structure in general.
no idea why would you follow this approach it is so weird and hard to follow and maintain but for the sake of your argument. the issue is that you are passing this to the bind while this has no property of state or setState so you should pass a mocked this instead.
here is how it could go
describe('helperFunction', () => {
function helperFunction() {
// #ts-ignore
this.setState({
// #ts-ignore
bar: 'calculated' + this.state.foo,
})
}
it('updates the provided stateHolder', () => {
const stateHolder = {
state: { foo: 'bar' },
// don't use arrow function otherwise it won't react the state via this
setState(obj: any) {
this.state = obj;
},
};
const importedHelperFunction = helperFunction.bind(stateHolder);
importedHelperFunction();
expect(stateHolder.state.foo).toBe('calculatedbar');
});
});

How to avoid repeatedly instantiating an object

I'm using reactjs and there is a instantiation in componentDidUpdate(). But when update state, the componentDidUpdate() will be execute and the Object will repeat statement
ps: const barrage = new Barrage(); the object will be execute repeatly
componentDidUpdate() {
const barrage = new Barrage();
}
const barrage = new Barrage();
how to avoid the Object statement repeatly execute
you haven't really explained what you want to achieve but I will try to solve this specific question by saying you can put it in a state if you need it to create new instance only once
constructor(props) {
super(props);
...
this.state = {
barrage : null
}
...
}
componentDidUpdate(prevState) {
if(!prevState.barrage) {
this.setState({ barrage : new Barrage() )}
}
}
You could create it in componentDidMount and cleanup in componentWillUnmount
class TestComponent extends React.Component {
barage = null;
componentDidMount() {
this.barage = new Barage();
}
componentWillUnmount() {
this.barage = null;
}
}
It's really hard to guess what you're doing with that object without any code. I can only assume that it is independent of the updates to the component, if any.

Adding methods to react context

So I have an app that looks like this (tried to simplify as much as possible):
// file with context
const SomeContext = React.createContext({});
// file with app root
class App extends Component {
constructor() {
super();
this.state = {
someValue: 123
};
}
doSomething() {
// this does not work,
// as "this" is context value
this.setState({
someValue: someValue + 1
});
}
render() {
// create context values
const value = {
someValue: this.state.someValue
doSomething: this.doSomething
};
return <SomeContext.Provider value={value}><ChildComponent/></SomeContext>;
}
}
// file with child component
class ChildComponent extends Component {
render() {
return <button onClick={this.context.doSomething} />
}
}
ChildComponent.contextType = SomeContext;
I want context to have doSomething method that updates state of the app (specifically increase someValue by 1).
How to correctly execute doSomething method from child component?
Do I have to do doSomething: this.doSomething.bind(this)? or there is a better way?
edit question is less about how to get "this", but more about what's the recommended way to do it in this case. Maybe I should not add method to context at all?

Move function in React from component to referenced library

I'm learning React and I'm not sure how to setup this pattern. It could be something really easy I'm just missing.
I have a main component that controls state. It has all of the functions to update state and passes these down to child components via props. I've simplified the code to focus on one of these functions.
Here's the component now, all works as it should:
ManageMenu.js
import React from 'react'
class ManageMenu extends React.Component {
constructor() {
super()
this.toggleEditing = this.toggleEditing.bind(this)
// Set initial state
this.state = {
menuSections: []
}
}
toggleEditing(id) {
const menuSections = this.state.menuSections
menuSections.map(key => (key.id === id ? key.details.editing = id : ''))
this.setState({ menuSections })
}
render() {
return (
...
)
}
}
export default ManageMenu
The toggleEditing is passed via props to a child component that uses it to render an editing form if the edit button is clicked.
I have about 10 of these different functions in this component and what I would like to do is move them to an external lib/methods.js file and then reference them. Below is the code I would like to have, or something similar, but React doesn't like what I'm doing. Throws a syntax error:
Failed to compile.
Error in ./src/components/ManageMenu.js
Syntax error: Unexpected token
toggleEditing(id, menuSectionId, this.state, this)
Here is what I would like to do...
lib/methods.js
const toggleEditing = function(id, state, that) {
const menuSections = state.menuSections
menuSections.map(key => (key.id === id ? key.details.editing = id : ''))
that.setState({ menuSections })
}
module.exports = {
toggleEditing
}
And then in my component:
ManageMenu.js
import React from 'react'
import { toggleEditing } from '../lib/methods'
class ManageMenu extends React.Component {
constructor() {
super()
// Set initial state
this.state = {
menuSections: []
}
}
toggleEditing(id, this.state, this)
render() {
return (
...
)
}
}
export default ManageMenu
Any help is appreciated, thanks!
Thanks to #Nocebo, the answer on how to externalize functions is here:
Externalise common functions in various react components
In my particular situation,
I need to remove the “floating” toggleEditing(id, this.state, this) call in the middle of nowhere. Update: This error happens “because it is invoking a method within a class definition.” (see Pineda’s comment below)
Remove the leading this. on the right side of the this.toggleEditing statement in constructor()
Update the function in lib/methods.js to remove the state and that variables since its bound to this in the constructor()
See updated code below.
ManageMenu.js
import React from 'react'
import { toggleEditing } from '../lib/methods'
class ManageMenu extends React.Component {
constructor() {
super()
this.toggleEditing = toggleEditing.bind(this)
// Set initial state
this.state = {
menuSections: []
}
}
render() {
return (
...
)
}
}
export default ManageMenu
lib/methods.js
const toggleEditing = function(id) {
const menuSections = this.state.menuSections
menuSections.map(key => (key.id === id ? key.details.editing = id : ''))
this.setState({ menuSections })
}
module.exports = {
toggleEditing
}
You're error arises because you are invoking toggleEditing in your ManageMenu.js class definition rather than defining a function.
You can achive what you want by setting a local class member this.toggleEditing to the bound function returned by the .bind method and do so within the constructor:
import React from 'react'
import { toggleEditing } from '../lib/methods'
class ManageMenu extends React.Component {
constructor() {
super()
this.state = {
menuSections: []
}
// bind external function to local instance here here
this.toggleEditing = toggleEditing.bind(this);
}
// don't invoke it here, bind it in constructor
//toggleEditing(id, this.state, this)
render() {
return (
...
)
}
}
export default ManageMenu

Why is my call to (mobx) extendObservable not triggering a re-render?

Here is the code -- pretty sure it is something about extendObservable that I just don't get, but been staring at it for quite a while now. When addSimpleProperty runs, it seems to update the object, but it doesn't trigger a render.
const {observable, action, extendObservable} = mobx;
const {observer} = mobxReact;
const {Component} = React;
class TestStore {
#observable mySimpleObject = {};
#action addSimpleProperty = (value) => {
extendObservable(this.mySimpleObject, {newProp: value});
}
}
#observer
class MyView extends Component {
constructor(props) {
super(props);
this.handleAddSimpleProperty = this.handleAddSimpleProperty.bind(this);
}
handleAddSimpleProperty(e) {
this.props.myStore.addSimpleProperty("newpropertyvalue");
}
render() {
var simpleObjectString =JSON.stringify(this.props.myStore.mySimpleObject);
return (<div>
<h3> Simple Object</h3>
{simpleObjectString}
<br/>
<button onClick={this.handleAddSimpleProperty}>Add Simple Property</button>
</div>);
}
}
const store = new TestStore();
ReactDOM.render(<MyView myStore={store} />, document.getElementById('mount'));
store.mySimpleObject = {prop1: "property1", prop2: "property2"};
This problem is brought up in the Common pitfalls & best practices section of the documentation:
MobX observable objects do not detect or react to property assignments
that weren't declared observable before. So MobX observable objects
act as records with predefined keys. You can use
extendObservable(target, props) to introduce new observable
properties to an object. However object iterators like for .. in or
Object.keys() won't react to this automatically. If you need a
dynamically keyed object, for example to store users by id, create
observable _map_s using
observable.map.
So instead of using extendObservable on an observable object, you could just add a new key to an observable map.
Example
const {observable, action} = mobx;
const {observer} = mobxReact;
const {Component} = React;
class TestStore {
mySimpleObject = observable.map({});
#action addSimpleProperty = (value) => {
this.mySimpleObject.set(value, {newProp: value});
}
}
#observer
class MyView extends Component {
constructor(props) {
super(props);
this.handleAddSimpleProperty = this.handleAddSimpleProperty.bind(this);
}
handleAddSimpleProperty(e) {
this.props.myStore.addSimpleProperty("newpropertyvalue");
}
render() {
var simpleObjectString = this.props.myStore.mySimpleObject.values();
return (
<div>
<h3> Simple Object</h3>
{simpleObjectString.map(e => e.newProp)}
<br/>
<button onClick={this.handleAddSimpleProperty}>Add Simple Property</button>
</div>
);
}
}
const store = new TestStore();
ReactDOM.render(<MyView myStore={store} />, document.getElementById('mount'));

Categories

Resources