Reactive models - javascript

Reactive View components (React, Angular, Vue, etc) revolutionized interface programming by eliminating the need to keep the view up to date with props/state. But as far as I know an analogous Model pattern has not been implemented/popularized (if Redux solves this problem, it's unclear to me how it can do so with classes).
The main area I'm running into a need for this (across many applications) is when some kind of constraining is involved. Some child/related model needs to be constrained when it is created, when it is updated, when the parent is updated, or when siblings are updated.
It could be done something like the following (just threw this together quickly to illustrate), but this isn't very efficient at scale because it's not selective (i.e. React only rerenders components that change).
Are there any implementations of this kind of data structure, or is this something I have to roll myself?
class ReactiveModel {
setState(newState) {
this.state = newState;
this.updateChildren();
}
setProps(newProps) {
this.props = newProps;
this.updateChildren();
}
updateChildren() {
var childrenSpecification = this.renderChildren();
//for each child
//create a new instance if one doesn't exist
//update props if any need to be updated
//updating props or state on a child triggers updateChildren on it so updating bubbles down
}
}
class Box extends ReactiveModel {
constructor(props) {
super(props);
this.state = {
boxObjects: this.props.boxObjectsFromApi //just plain data - "Object" instances, not "Box" instances
}
}
addBox(boxObject) {
this.setState({boxObjects: [...this.state.boxObjects, boxObject]}); //should call updateChildren
}
renderChildren() {
return {
boxes: this.state.boxObjects.map(({width}) => {
return {
class: Box,
props: {
width: Math.min(this.props.width, width) //constrain child width to be inside parent width
}
};
}
}
}
}

Related

Can you use a backdraftjs watchable to make a component completely re-render?

This is a contrived example but it is similar to real-life situations where, for example, you might have a list of links built from data that you are AJAXing in from a server.
import {Component, e, render} from './node_modules/bd-core/lib.js';
// a list of strings that for alert when you click them
class AlertLinkList extends Component.withWatchables('data') {
handleClick(event){
alert(event.target.innerHTML);
}
bdElements() {
return e.div(
{
// bdReflect: ?????
},
['yes', 'no', 'maybe'].map(
txt => e.div({bdAdvise: {click: 'handleClick'}}, txt)
)
)
}
}
var linkList = render(AlertLinkList, {}, document.body);
// I would like to change the strings but this (obviously) does nothing
linkList.data = ['soup', 'nuts', 'fish', 'dessert'];
I can't think of a straightforward way to solve this.
bdReflect only works on writable DOM attributes, I think, so for example I could use it to replace the innerHTML of the component but then I think I lose the bdAdvise assignments on the links (and it also seems kinda kludgey).
Any ideas?
OK here's one pattern that works for this...
get rid of the watchables in AlertLinkList
instead, use kwargs to populate the list
wrap the list in another component that simply re-renders the list with new content whenever the content changes (e.g. after fetching new content from the server)
// a list of strings that alert when you click them
class AlertLinkList extends Component {
handleClick(event){
alert(event.target.innerHTML);
}
bdElements() {
return e.div(
this.kwargs.items.map(
txt => e.div({bdAdvise: {click: 'handleClick'}}, txt)
)
)
}
}
// a wrapper that provides/retrieves data for AlertLinkList
class LinkListWrapper extends Component {
bdElements() {
return e.div(
{},
e.a(
{bdAdvise: {click: 'updateList'}},
'Click to Update List',
),
e.div({bdAttach: 'listGoesHere'}),
);
}
updateList(event) {
// the data below would have been retrieved from the server...
const resultRetrievedFromServer = ['soup', 'nuts', 'fish', 'dessert'];
this.renderList(resultRetrievedFromServer)
}
renderList(items) {
render(AlertLinkList, {items}, this.listGoesHere, 'only')
}
postRender() {
const initialData = ['yes', 'no', 'maybe']
this.renderList(initialData);
}
}
var linkList = render(LinkListWrapper, {}, document.body);
The only issue I see here is that it may be suboptimal to re-render the entire wrapped component if only one small part of the data changed, though I suppose you could design around that.
Let's begin solving this problem by describing the public interface of AlertLinkList:
A component that contains a homogeneous list of children.
The state of each child is initialized by a pair of [text, url].
The list is mutated en masse.
Given this, your start is almost perfect. Here it is a again with a few minor modifications:
class AlertLinkList extends Component.withWatchables('data') {
handleClick(event) {
// do something when one of the children is clicked
}
bdElements() {
return e.div({}, this.data && this.data.map(item => e(AlertLink, { data: item })));
}
onMutateData(newValue) {
if (this.rendered) {
this.delChildren();
newValue && newValue.forEach(item => this.insChild(AlertLink, { data: item }));
}
}
}
See https://backdraftjs.org/tutorial.html#bd-tutorial.watchableProperties for an explanation of onMutateData.
Next we need to define the AlertLink component type; this is trivial:
class AlertLink extends Component {
bdElements() {
return e.a({
href: this.kwargs.data[1],
bdAdvise: { click: e => this.parent.handleClick(e) }
}, this.kwargs.data[0]);
}
}
The outline above will solve your problem. I've written the pen https://codepen.io/rcgill/pen/ExWrLbg to demonstrate.
You can also solve the problem with the backdraft Collection component https://backdraftjs.org/docs.html#bd-core.classes.Collection
I've written a pen https://codepen.io/rcgill/pen/WNpmeyx to demonstrate.
Lastly, if you're interested in writing the fewest lines of code possible and want a fairly immutable design, you don't have to factor out a child type. Here's a pen to demonstrate that: https://codepen.io/rcgill/pen/bGqZGgW
Which is best!?!? Well, it depends on your aims.
The first solution is simple and general and the children can be wrangled to do whatever you want them to do.
The second solution is very terse and includes a lot of additional capabilities not demonstrated. For example, with the backdraft Collection component
mutating the collection does not destroy/create new children, but rather alters the state of existing children. This is much more efficient and useful when implementing things like large grids.
you can mutate an individual elements in the collection
The third solution is very terse and very fixed. But sometimes that is all you need.

Extend object created by parent class

I have a couple of d3 charts that share quite a lot of functionality. However they do diverge in a several areas. I'm considering moving them to separate classes based off a parent class because the singular buildChart function's length has grown unmaintainable.
The problem I'm facing right now is that even though it seems pretty easy to override certain methods from the parent in some instances, it would be nice to have the parent build most of one setup object, which contains properties like tick intervals, display formats, etc, but have each child class add a few properties to that object.
I thought to do this in the following way:
class Chart {
constructor({ svg, data, range }) {
this.svg = svg;
this.range = range;
this.data = data;
this.setDetails();
}
setDetails() {
this.details = {
sharedProp: "this property is shared"
};
}
scaffoldChart() {
/* initial d3 stuff */
}
}
export class SimilarChartUno extends Chart {
constructor({ svg, data, range }) {
super({ svg, data, range });
this.decorateDetails()
super.scaffoldChart()
}
decorateDetails() {
this.details = Object.assign(this.details, {
someUniqueProp: 'prop for UNO a'
})
}
}
// instance details :
{
"sharedProp": "this property is shared",
"someUniqueProp": "prop for UNO A"
}
This seems to work, but I have not seen an example like this anywhere: Is there is a better pattern than this?
Code Sandbox
There's nothing wrong with doing it the way you are currently, but if you'd like to avoid adding methods you could alter your SimilarChartUno to be something like this:
class SimilarChartUno extends Chart {
constructor({ svg, data, range }) {
super({ svg, data, range });
super.scaffoldChart()
}
setDetails() {
super.setDetails();
this.details.someUniqueProp = 'prop for UNO a';
}
}
In this way, you've maintained inheritance. The setDetails() is invoked in the super constructor with the child implementation. Alternatively, you can still perform an this.details = Object.assign(this.details, { complexObject: {} }); after the super.setDetails(); call for more complex assignments.

How to modify value passed by props

I'm a begginer in React and would like to figure out how to modify values get using props.
f.e:
I have a MobX GameStore.tsx with #observable values:
export class GameStore {
#observable money = 0;
#observable CPS = 0;
#observable taskCodeLines = 0;
#observable taskCodeLinesTarget = 10;
...
#observable staffFrontEndCount = 4;
#observable staffFrontEndStartCost = 100;
#observable staffPHPCount = 2;
#observable staffPHPStartCost = 250;
}
Now I want to have a few StaffMember objects in Staff class:
render() {
return(
<div className="staff">
<ul className="staff-list">
<StaffMember job="Front End Developer" count={ gameStore.staffFrontEndCount } startCost = { gameStore.staffFrontEndStartCost } />
<StaffMember job="PHP Developer" count={ gameStore.staffPHPCount } startCost = { gameStore.staffPHPStartCost } />
</ul>
</div>
);
}
I pass down a data like name of this objects and some values. And now I want to modify some of them, like:
#observer
export default class StaffMember extends React.Component<any, any> {
#computed get increaseStaffCount() {
return this.props.count;
}
#action hireStaff() {
let cost = this.props.startCost * 1.4 * (this.props.count + 1);
if (gameStore.money >= cost) {
gameStore.money -= cost;
this.props.count += 1; // It's illegal because props data is read-only
this.countCPS();
}
}
How can I do this? Is this OK to create logic like above?
How should I create instances of classes in react and build a generic methods for them?
Thanks for help ;)
React does not allow the modification of props values over the course of a component's life. And there are currently two ways it has gotten around the need to change the value of props.
Load it into a state
Utilize Redux
On the first item, as xSkrappy said before, you can load the props into a Component's state, which can be updated over the course of a component's life, adding this method inside the Component in the following manner:
componentDidMount() {
this.setState({ count: this.props.count })
}
This creates a local state in the component that is equal to the prop value that was passed down to the component from its parent. And you can begin to change it from there.
You can also use the componentWillReceiveProps lifecycle method to re-render the component when the props value changes in its parent component, like such:
componentWillReceiveProps(nextProps) {
if(nextProps.count !== this.props.count) {
this.setState({ count: nextProps.count })
}
}
The second method involves utilizing Redux, a state container that can be used in React applications. Its pattern involves creating a store where the state of the entire application can be managed, and any given component can be connected to that store and receive that state as props.
While utilizing Redux is a lot more complex than the first option given, in the end you are given a lot more freedom because you can make your count value accessible to any component in your application!
Sadly implementing Redux is too lengthy a process to just detail in this answer, so I'll direct you to what I think is a good guide to refactoring your application to use Redux, should you wish to go with this option
The answer to that would be after passing the props inside StaffMember put it inside a state then from there you can modify the state :)
In ReactJs, Props are immutable so you can't modify it. Instead of using Props You can use State. State are mutable you can modify it. Or, you can use Redux concept as per your requirement.
For ex:- First make a state
this.state = {
usersList:[]
};
then you can add modification in your state like this
componentDidMount() {
this.setState({ usersList: this.props.count})
}

React componentWillReceiveProps not updating state

I've got this React parent component here. The children components at this point are just returning dropdown menus. I expected that componentWillReceiveProps would update the state here, which in turn should be passed to StopList as props. However, when state.selectedSub is changed through handleSubSelect, nothing happens and StopList doesn't receive any props.
Is my mistake with the asynchronous nature of componentWillReceiveProps? Is it in the wrong place in my code? Am I using the wrong lifecycle method?
// We're controlling all of our state here and using children
// components only to return lists and handle AJAX calls.
import React, { Component } from 'react';
import SubList from './SubList';
import StopList from './StopList';
class SubCheck extends Component {
constructor (props) {
super(props);
this.state = {
selectedSub: '--',
selectedStop: null,
stops: ['--'],
};
this.handleSubSelect.bind(this);
this.handleStopSelect.bind(this);
}
// We want the user to be able to select their specific subway
// stop, so obviously a different array of stops needs to be
// loaded for each subway. We're getting those from utils/stops.json.
componentWillReceiveProps(nextProps) {
var stopData = require('../utils/stops');
var stopsArray = [];
var newSub = nextProps.selectedSub
for(var i = 0; i < stopData.length; i++) {
var stop = stopData[i];
if (stop.stop_id.charAt(0) === this.state.selectedSub) {
stopsArray.push(stop.stop_name);
}
}
if (stopsArray.length !== 0 && newSub !== this.state.selectedSub) {
this.setState({stops: stopsArray});
}
}
handleSubSelect(event) {
this.setState({selectedSub:event.target.selectedSub});
}
handleStopSelect(event) {
this.setState({selectedStop:event.target.selectedStop})
}
render() {
return (
<div>
<SubList onSubSelect={this.handleSubSelect.bind(this)}/>
<StopList stops={this.state.stops} onStopSelect={this.handleStopSelect.bind(this)}/>
</div>
);
}
}
export default SubCheck;
You are duplicating data, and causing yourself headaches that aren't necessary.
Both selectedSub and selectedStop are being stored as props and as state attributes. You need to decide where this data lives and put it in a singular location.
The problem you are encountering entirely revolves round the fact that you are changing the state attribute and expecting this to trigger a change to your props. Just because they share a name does not mean they are the same value.

How to access one component's state from another component

How do I access one component's state in another component? Below is my code and I'm trying to access the state of component a in component b.
var a = React.createClass({
getInitialState: function () {
return {
first: "1"
};
},
render: function () {
// Render HTML here.
}
});
var b = React.createClass({
getInitialState: function () {
return {
second: a.state.first
};
},
render: function () {
// Render HTML here.
}
});
But I'm not getting anything.
Even if you try doing this way, it is not correct method to access the state. Better to have a parent component whose children are a and b. The ParentComponent will maintain the state and pass it as props to the children.
For instance,
var ParentComponent = React.createClass({
getInitialState : function() {
return {
first: 1,
}
}
changeFirst: function(newValue) {
this.setState({
first: newValue,
});
}
render: function() {
return (
<a first={this.state.first} changeFirst={this.changeFirst.bind(this)} />
<b first={this.state.first} changeFirst={this.changeFirst.bind(this)} />
)
}
}
Now in your child compoenents a and b, access first variable using this.props.first. When you wish to change the value of first call this.props.changeFirst() function of the ParentComponent. This will change the value and will be thus reflected in both the children a and b.
I am writing component a here, b will be similar:
var a = React.createClass({
render: function() {
var first = this.props.first; // access first anywhere using this.props.first in child
// render JSX
}
}
If two components need access to the same state they should have a common ancestor where the state is kept.
So component A is the parent of B and C.
Component A has the state, and passes it down as props to B and C.
If you want to change the state from B you pass down a callback function as a prop.
I would suggest you use a state manager like Redux (personal favorite), MobX reflux, etc to manage your state.
How these works is they allow you to contain all shared state in one state storage (called a store), and whatever component needs access to a part of that shared state, it will just get it from the store.
It looked very hard to get started with but once you get over the small challenges, get 2 or 3 "wtf's" out of the way. It gets easier.
Take a look here: http://redux.js.org/
EDIT: Redux is good but the boilerplate code is really a turn off... for those of you looking for a simpler, more magical (this can be good and bad) solution use mobx : https://mobx.js.org/
in the child component create function that sets the state:
changeTheState(){
this.setState({something:"some value"})
}
and in parent component give the child a ref as following:
<Child ref={component => this._child = component}/>
then in parent make a function to access the changeTheState()
parentFunction(){
this._child.changeTheState();
}
and just use the parentFunction.
If you have A and B component where B is a child of A, you can pass a function to change the state of A though props to B.
function B(props) {
return <button onClick={props.changeA} />
}
class A extends React.Component {
//constructor
//pass this function to B to change A's state
handleA() {
this.setState({});
}
render() {
return <B changeA={() => this.handleA()} />
}
}
Take a look at React Context
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
You can also update Context from a nested component if required.

Categories