When to really use getDerivedStateFromProps? - javascript

I'm having trouble determining if my component hierarchy really needs getDerivedStateFromProps, if the cases where it is needed is really as rare as the documentation makes it sound. It might be a fundamental misunderstanding about React/Redux design.
class AttributeList extends React.Component {
constructor(props){
super(props)
this.state = {
attributes: props.attributes,
newAttributes: []
}
}
addNewAttribute = () => {
// add new empty attribute to newAttributes state
}
onKeyChange = () => {
// update appropriate attribute key
}
onValueChange = () => {
// update appropriate attribute value
}
saveAttributes = () => {
// save the, API call
}
render = () => {
this.state.attributes.map((pair) => {
<Attribute
//pass data + functions, functional component />
})
this.state.newAttributes.map((pair) => {
<Attribute
//pass data + functions, functional component />
})
}
static getDerivedStateFromProps(){
// ?? do comparisons here to choose to remove or keep certain newAttributes? or just ignore result of save and keep interface as-is, just show error message if saving failed.
}
}
I have a parent component AttributeList which renders a bunch of Attributes, which are essentially key-value pairs. AttributeList receives the list of attributes of a document as props. However, the attributes can be edited, so it initializes its state (this.state.attributes) with this.props.attributes. Normally keys are immutable, but if a user adds a new attribute to the list, he can edit both the key and value. At any point, a user can save all the attributes. When the new attributes are saved, I'd like to disabled editing the keys for them as well. Here is the dilemma.
Option one is to save the document and just hope it worked, and then clear the new attributes list and mark all the attributes as saved (disabling the key input). I think this would be the "fully uncontrolled" solution, where once the state is initialized the component deals with everything on it's own. However, what if the save fails? I don't want to show and incorrect state to the user.
So I want to do option two. After save, fetch the document, which will load the attribute list and re-render the component. However I need to get rid of my new attributes since they are now a part of the attributes prop. I would like to verify that the new attributes are actually a part of the attributes prop now. It seems like this would happen ingetDerivedStateFromProps where I would on each render cycle check if any new attribute keys already exist in the attributes prop, and remove them from the "new" list if they do, and return that state.
But is this really the right time to use getDerivedStateFromProps? It seems to me that for any page that a user is "editing" something where you make an API call to save it, if you want to render based on the saved data ("the truth"), then I need to use getDerivedStateFromProps. Or perhaps from a design perspective it is better to show a message akin to "data not successfully saved" and keep the state as is, to prevent any data loss. I'm honestly not sure.

I don't see how getDerivedStateFromProps comes into it as there's no reason you need to copy props into state is there? When an old attribute value is changed you save it to the redux store, when new attribute properties are changed you can update local state (or save them to a different slice of the store, or differentiate them some other way). Update rules can be enforced in the update handlers or during merge on save.
// dispatch redux action to update store
onOldValueChange = () => {}
// this.setState to update new value
onNewKeyChange = () => {}
onNewValueChange = () => {}
render = () => {
this.props.attributes.map((pair) => {
<Attribute
//pass data + onOldValueChange, functional component />
})
this.state.newAttributes.map((pair) => {
<NewAttribute
//pass data + functions, functional component />
})
}

Related

Reactivity on props Vue.js

I am in the beginning of learning Vue, and having a hard time understanding how to define props etc. using the composition API.
I have a component (ACE Editor), loaded like this:
<v-ace-editor
v-model:value="currentRecord.text"
/>
The v-ace-editor needs to have the model and value loaded like this: v-model:value.
import {computed, ref} from 'vue';
import collect from "collect.js"
const props = defineProps({
records: {
type: Object,
},
})
//Get all records.
let getRecords = () => {
return collect(props.records)
}
//Filter the records using "collect.js"
const getUnlabeledRecords = () => {
return getRecords()
.where('skipped', false)
.where('processed', false);
}
//Assign all unlabeled records on the first load.
let allRecords = ref(getUnlabeledRecords());
//In order to check if "text" is empty - and if not, set a default value, load the first record into a temp. variable.
let first = allRecords.value.first();
if(first){
first.text ??= "";
}
//Load the current record into a ref.
let current = ref(first);
//Finally, compute it.
let currentRecord = computed(() => current.value)
Looking at this, and coming from a backend background, it feels very bloated.
I have tried the following:
let allRecords = ref(getUnlabeledRecords());
let currentRecord = computed(() => allRecords.value.first())
But doing this leads to me not being able to interact with the currentRecord - nor change the allRecords. This means that if for example currentRecord.text is null from the backend, my ace-editor component fails because it expects a value.
Is there another way to load in these variables?
You actually don't have to called .value of a ref when using it in the template.
So you can actually remove the computed part (last line of your ) and change your template to.
<v-ace-editor
v-model="current.text"
/>
Now, assuming you managed v-model correctly in v-ace-editor (if this is your own component), you should have reactivity kept when modifiying current.text from v-ace-editor.
As a side note, computed properties are read-only. You cannot expect a child component to modify its value by passing it with v-model.
However, you should note that updating records prop from parent component will not update current. For this, maybe you want to add a watcher on records.
Also, personal suggestion, but if you only really care about currentRecord in your component and not all records, maybe you should do the filtering from parent component and only pass currentRecord as a prop. Other personal suggestion, you can declare all your variables in your script with const instead of let. const prevent reassignation, but since you work with refs, you never reassign it, but you change its value property.

React best practice for changing state of parent component from child without rerendering all children?

I have a project where I'm displaying cards that contain attributes of a person in a textfield, and the user can edit the textfield to directly change that person's attribute values. However every time they're editing the textfield it causes a rerender of all cards which slows down the app. Here is an example:
export default Parent() {
const [personList, setPersonList] = useState(/* list of person objects*/);
const modifyPerson(index, property, value) {
const newPersonList = _.cloneDeep(personList);
newPersonList[index][property] = value;
setPersonList(newPersonList);
}
const children = personList.map((person, index) => {
<Person
modifyPerson={modifyPerson}
index=index
/*properties on the person */
/>
});
return <div> {children} </div>
}
export default Person(props) {
const fields = /* assume a list of these textfields for each property */
<TextField
value={props.name}
onChange={(e) => modifyPerson(props.index,"name",e.target.value)}
value={props.name} >
return {fields};
}
So essentially when the child's text field is updated, it triggers a state change in the parent that stores the new value, then refreshes what the Child looks like. There's no button that the user clicks to "save" the values-- as soon as they edit the textfield it's a permanent change. And also the parent needs to know the new values of every Person because there's some functions that require knowledge of the current state of the person list. The Person component contains images that slow down the rendering if done inefficiently.
Is there a better way to make this design more performant and reduce rerenders? I attempted to use useCallback to preserve the functions but I don't it works in this specific design because the property and values are different-- do I have to create a new "modifyPerson" for each exact attribute?
Use React.memo()
React.Memo will check the props passed to the component and only if the props changes , it will re-render that particular component.
So, if you have multiple Person component which are getting props which are explicit to those Person, and when you change a particular Person component which leads to Parent state getting updated, only that Person component which you had modified will re-render because its props will change(assuming that you pass the changed value to it).
The other components will have the same props and wont change.
export default React.memo(Person(props) {
const fields = /* assume a list of these textfields for each property */
<TextField
value={props.name}
onChange={(e) => modifyPerson(props.index,"name",e.target.value)}
value={props.name} >
return {fields};
})
As others have already said React.memo() is the way to go here, but this will still re-render because you recreate modifyPerson every time. You can use useCallback to always get the same identity of the function, but with your current implementation you would have to add personList as dependency so that doesn't work either.
There is a trick with setPersonList that it also accepts a function that takes the current state and returns the next state. That way modifyPerson doesn't depend on any outer scope (expect for setPersonList which is guaranteed to always have the same identity by react) and needs to be created only once.
const modifyPerson = useCallback((index, property, value) {
setPersonList(currentPersonList => {
const newPersonList = _.cloneDeep(currentPersonList);
newPersonList[index][property] = value;
return newPersonList;
})
}, []);

Is it possible to use references as react component's prop or state?

I want to create react table component which values are derived from single array object. Is it possible to control the component from view side? My goal is that every user using this component in their web browsers share the same data via singleton view object.
Program modeling is like below.
Database - there are single database in server which contain extinct and independent values.
DataView - there are singleton View class which reflects Database's table and additional dependent data like (sum, average)
Table - I'll build react component which looks like table. And it will show View's data with supporting sorting, filtering, editing and deleting row(s) feature (and more). Also it dose not have actual data, only have reference of data from View(Via shallow copy -- This is my question, is it possible?)
My intentions are,
- When user changes value from table, it is queried to DB by View, and if succeed, View will refer updated data and change it's value to new value and notify to Table to redraw it's contents. -- I mean redraw, not updating value and redraw.
- When values in View are changed with DB interaction by user request, there are no need to update component's value cause the components actually dose not have values, only have references to values (Like C's pointer). So only View should do is just say to Component to redraw it's contents.
I heard that React's component prop should be immutable. (Otherwise, state is mutable) My goal is storing references to component's real value to it's props so that there are no additional operation for reflecting View's data into Table.
It is concept problems, and I wonder if it is possible. Since javascript dose not support pointer officially(Am I right?), I'm not sure if it is possible.
View class is like below,
const db_pool = require('instantiated-singleton-db-pool-interface')
class DataView {
constructor() {
this.sessions = ['user1', 'user2'] // Managing current user who see the table
this.data = [ // This is View's data
{id:1, name:'James', phone:'12345678', bank:2000, cash:300, total:2300,..},
{id:2, name:'Michael', phone:'56785678', bank:2500, cash:250, total:2300,..},
{id:3, name:'Tyson', phone:'23455432', bank:2000, cash:50, total:2300,..}
] // Note that 'total' is not in db, it is calculated --`dependent data`
}
notifySessionToUpdate(ids) {
// ids : list of data id need to be updated
this.sessions.forEach((session) => {
session.onNotifiedUpdateRow(ids) // Call each sessions's
})
}
requestUpdateRow(row, changed_value) {
// I didn't write async, exception related code in this function for simple to see.
update_result = db_pool.update('UPDATE myTable set bank=2500 where id=1')
if (update_result === 'fail') return; // Do Nothing
select_result = db_pool.select('SELECT * from myTable where id=1') // Retrieve updated single data which object scheme is identical with this.data's data
for (k in Object.keys(select_result)) {.ASSIGN_TO_row_IF_VALUE_ARE_DIFFERENT.} // I'm not sure if it is possible in shallow copy way either.
calc.reCalculateRow(row) // Return nothing just recalculate dependant value in this.data which is updated right above.
// Notify to session
this.notifySessionToUpdate([1]) // Each component will update table if user are singing id=1's data if not seeing, it will not. [1] means id:1 data.
return // Success
}
... // other View features
}
Regarding session part, I'm checking how to implement sessionizing(?) the each user and it's component who is communicating with server. So I cannot provide further codes about that. Sorry. I'm considering implementing another shallow copied UserView between React Component Table and DataView(And also I think it helps to do something with user contents infos like sorting preference and etc...)
Regarding DB code, it is class which nest it's pool and query interface.
My problem is that I'm not familiar with javascript. So I'm not sure shallow copy is actually implementable in all cases which I confront with.
I need to think about,
1. Dose javascript fully support shallowcopy in consistent way? I mean like pointer, guarantee check value is reference or not.
2. Dose react's component can be used like this way? Whether using props or state Can this be fullfilled?
Actually, I strongly it is not possible to do that. But I want to check your opinions. Seems it is so C language-like way of thinking.
Redraw mean re-render. You can expose setState() or dispatch() functions from Table component and call them on View level using refs:
function View() {
const ref = useRef();
const onDbResponse = data => ref.current.update(data);
return (
<Table ref={ ref } />
);
}
const Table = React.forwardRef((props, ref) => {
const [ data, setData ] = useState([]);
useImperativeHandler(ref, {
update: setData
});
...
});
Anyway i don't think it's a good practice to update like that. Why can't you just put your data in some global context and use there?
const Context = React.createContext({ value: null, query: () => {} });
const Provider = ({ children }) => {
const [ value, setValue ] = useState();
const query = useCallback(async (request) => {
setValue(await DB.request(request));
}, [ DB ]);
const context = { value, query };
return <Context.Provider value={ context }>{ children }</Context.Provider>;
}
const useDB = () => useContext(Context);
const View = () => {
const { request } = useDB();
request(...);
}
const Table = () => {
const { value } = useDB();
...
}

React.js: Parent state values not passing into child properties + Fetch API data cannot be accessed

I am encountering several issues in a very basic color harmony picker I am developing. I am still a beginner in React and JSX. I initially had it put up on GitHub so the full files are on there, but I moved it over to Codepen instead.
Here is the Codepen
I made a lot of comments so sorry if they're a bit much, but hopefully they help. My problems don't begin until line 41, the displayHarmonies() method of the DataStore class. The values passed to it come from my App (parent) component:
displayHarmonies(color, harmony) {
//color and harmony pass in dynamically just fine...this.data will not return anything, not even "undefined"
console.log(color + " is the color and " + harmony + " is the harmony...and dataStore.displayHarmonies says: " + this.data);
this.registeredWatchers.map((watcher) => {
let result = "not green"; //result and resultHex will be determined with an underscore statement that will associate the color & harmony choice (primary + foreign key concept) and will return correct harmony color(s)
let resultHex = "#HEX";
appState.harmonyColor = result;
appState.harmonyHex = resultHex;
//call to app component's onDataChange() method, where new states will be set using the the appState data we just set in lines 49 and 50
watcher.onDataChange();
})
}
As you can see from my first comment, the only part that doesn't log to the console is this.data, which is set in the constructor for the DataStore:
constructor(data) {
//store that data in the object
//data is not being received from object instance of dataStore on line 187
this.data = data;
On line 187 I make an instance of the DataStore and pass it a variable named data. Prior to being used, this variable is initialized and then assigned to parsed JSON data via Fetch API:
let data = [];
//use polyfill for older browsers to do Ajax request
fetch("data/data.json").then((response) => {
//if we actually got something
if (response.ok) {
//then return the text we loaded
return response.text();
}
}).then((textResponse) => {
data = JSON.parse(textResponse);
});
If I console out the data in the second fetch .then() method, the JSON comes back just fine. As soon as I try to use the data variable anywhere else in the application, it returns nothing, as shown in the displayHarmonies() method's console.log(). So that's my first issue, but before I wanted to get to that, I wanted to solve the other issue I was having.
After the appState object (initialized prior to the DataStore, under the fetch statement) values get set to the result variables, displayHarmonies() runs watcher.onDataChange() (in the App component/parent) where the harmonyColor and harmonyHex states get assigned to the new appState values:
onDataChange() {
console.log("onDataChange() in App called");
this.setState({
harmonyColor: appState.harmonyColor,
harmonyHex: appState.harmonyHex
})
}
If I log these states out to the console, they are the right values, so that's not the problem. I then pass my states to the Display child component to be used as properties:
<Display colorChoice={this.state.currentColor} harmonyChoice={this.state.currentHarmony} harmonyColor={this.state.harmonyColor} harmonyHex={this.state.harmonyHex} />
I then set the Display component states in the constructor, assigning them to the props that are being sent to it with each new rendition of the application. I then display the data onto the DOM with the Display component's render method. What's odd is that the application will display the initial states (color: red, harmony: direct, harmonyColor: green, etc.) just fine, but as soon as a change is made, the data on the DOM does not update. The initial data is loaded in the same way though: by passing the parent's states into the child's properties. I have a few console.log()s in place that seem to prove why this should work, however, it does not. So what am I doing wrong?
Thanks, and hope this is not too much for one question!
First a bit to your current code, at the end of the post, I have added an alternative solution, so if this is tl;dr; just skip to the snippet at the end :)
A first remark would be on the data variable that you wish to pass on to your DataStore, nl (I left out some parts, as they are irrelevant to the discussion)
let data = [];
fetch("data/data.json").then(( response ) => {
data = JSON.parse( response.text() );
});
//... later down the code
var store = new DataStore(data);
Here you are reassigning the data variable inside the then promise chain of your fetch call. Although the assignment will appear to work, the data that now is on store.data will be an empty array, and the global variable will data will now contain the parsed response.text(). You should probably just push in the data you have just parsed (but in my example, I didn't even include the DataStore so this is just for future reference)
In your CodePen, you seem to mixing props & state for your Display component. That is in essence a no-op, you shouldn't mix them unless you really know what you are doing. Also note, that by calling this.setState inside the componentWillReceiveProps life cycle method, the app will automatically re-render more than needed. I am referring to this code:
componentWillReceiveProps(nextProps) {
this.setState({
color: nextProps.colorChoice,
harmony: nextProps.harmonyChoice,
harmonyColor: nextProps.harmonyColor,
harmonyHex: nextProps.harmonyHex
});
}
But you are then rendering like this:
render() {
return (
<div>
{/* these aren't changing even though states are being set */}
<p><b>Color:</b> {this.state.color}</p>
<p><b>Harmony:</b> {this.state.harmony}</p>
<p><b>Harmony Color(s):</b> {this.state.harmonyColor} ({this.state.harmonyHex})</p>
</div>
)
}
Here you should remove the componentWillReceiveProps method, and render values from this.props as you are passing these along from your App.
Alternative solution
As mentioned in the comments, your code currently is doing a lot more than it should do to pass state between parent and child components.
One thing you should keep in mind, is that when a component state gets changed, react will re-render the component automatically. When it sees that the virtual DOM has discrepancies with the real DOM it will automatically replace those components.
In that sense, your DataStore is not necessary. Depending on how you want to manage state, the component will react on those changes.
Since your app uses Component State (which is fine for small applications, once you want to move to bigger applications, you will probably want to move on to something like Redux, or MobX), the only thing you need to do, is to make sure that you set the correct components state to trigger the rendering.
As an example, I remade your code in a cleaner way:
const Choice = ({ header, values, onChange, activeValue }) => {
return <ul>
<li><h1>{ header }</h1></li>
{ values.map( (value, key) => <li
key={key+value}
className={classNames( { active: value === activeValue, item: true } )}
onClick={() => onChange( value )}>{ value }</li> ) }
</ul>
};
const colors = ['red', 'green', 'black', 'blue', 'yellow'];
const harmonies = ['direct', 'split', 'analogous'];
class App extends React.Component {
constructor(...args) {
super(...args);
this.state = {
activeColor: undefined,
activeHarmony: undefined
};
}
onColorChanged( color ) {
this.setState({ activeColor: color });
}
onHarmonyChanged( harmony ) {
this.setState({ activeHarmony: harmony });
}
render() {
let { activeColor, activeHarmony } = this.state;
return <div>
<Choice
header="Choose color"
values={colors}
activeValue={activeColor}
onChange={(...args) => this.onColorChanged(...args)} />
<Choice
header="Choose harmony"
values={harmonies}
activeValue={activeHarmony}
onChange={(...args) => this.onHarmonyChanged(...args)} />
</div>;
}
}
ReactDOM.render( <App />, document.querySelector('#container'));
h1 { margin: 0; padding: 0; }
ul {
list-style-type: none;
}
.item {
cursor: pointer;
padding: 5px;
}
.active { background-color: lightgreen; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.6.0/prop-types.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.js"></script>
<div id="container"></div>
Now, there are some things in this sample code that might need some explanation. For one, this code has 2 component types, 1 presentational component called Choice which is stateless, and one container component called App which delegates it's state to it's children.
A bit more information about container & presentational components can be found on the blog of Dan Abramov (redux creator)
The essence of the above concept is just this, the App component is responsible for the state, and for sharing it with it's children. So, all state changes need to be made on the App component. As you can see in the render, the App simply passes its state along:
render() {
let { activeColor, activeHarmony } = this.state;
return <div>
<Choice
header="Choose color"
values={colors}
activeValue={activeColor}
onChange={(...args) => this.onColorChanged(...args)} />
<Choice
header="Choose harmony"
values={harmonies}
activeValue={activeHarmony}
onChange={(...args) => this.onHarmonyChanged(...args)} />
</div>;
}
The App passes a change handler along to the Choice component that can be called when a selection should occur, this gets forwarded to the App, the state changes, and app re-renders, allowing the Choice component to update it's elements.
const Choice = ({ header, values, onChange, activeValue })
Based on the props passed into it, the Choice component can decide which is the active item at the moment of rendering. As you can see, the props are destructed. header, values, onChange and activeValue are all properties on the props of the component, but to save time, we can assign these values at ones to a variable and use them in the rendering.
I tried cloning your repo, but it seems to be nested in another repo. With your current setup, this may work:
In your App component, you can put this lifecycle method to fetch the data, and then set the state with the received data.:
componentDidMount(){
fetch("data/data.json").then((response) => {
//if we actually got something
if (response.ok) {
//then return the text we loaded
return response.text();
}
}).then((textResponse) => {
this.setState({
data : JSON.parse(textResponse);
})
});
}
In the return statement, you can render the data store as a child so App can pass the data like this:
return (
<div className="App">
<DataStore data={this.state.data} />
<h1>Color Harmonies</h1>
{/* assigns this.colorChosen() & this.harmonyChosen() methods as properties to be called in Picker component */}
<Picker colorChosen={this.colorChosen.bind(this)} harmonyChosen={this.harmonyChosen.bind(this)}/>
{/* give Display component props that are dynamically set with states */}
<Display colorChoice={this.state.currentColor} harmonyChoice={this.state.currentHarmony} harmonyColor={this.state.harmonyColor} harmonyHex={this.state.harmonyHex} />
</div>
);
Then, your data store should receive the data as a prop, so you can use it like this:
displayHarmonies(color, harmony) {
//color and harmony pass in dynamically just fine...this.data will not return anything, not even "undefined"
console.log(color + " is the color and " + harmony + " is the harmony...and dataStore.displayHarmonies says: " + this.props.data); //data is received in the properties so you can use it.
//other code
})
Doing this, you should also be able to remove this.data from the constructor of the DataStore component.
Also in Data store, youll want to to allow it to accept props like this:
constructor(props){
super(props)
}

How to update Baobab leaf data before React component render?

ReactJS, Baobab, Material-UI app displays some items, identified by their numeric id. To display those, title and image url's are retrieved from a remote service via ajax. Tree branch stores that data:
data: {
12345: {title:'ABC', image:'https://...'}, // id is 12345
12346: {...
}
Upon item component creation and first rendering, its extended data may, or may not be already available in the tree. If its not, ajax call is enqueued to receive that data. It might happen that multiple items are created with the same item id.
To avoid extra requests for the same id, I want to put a dummy info {title:'loading', image:'spinner.gif'} into the tree upon the first request to that id's info. Thus this data will be used for the very first render(). Successive components would get that dummy info, and will not initiate any extra requests.
Question: how, and where can I place the code to test if the tree has no info yet and place the dummy there to indicate its "penging" state and enqueue the request?
Tried so far:
component's constructor – props are not set there yet;
componentWillMount() – the first render started with the old state of the tree, despite the tree.commit() after setting the dummy value;
in the branch function that dynamically creates components cursor pointing to its data. Got warning:
setState(...): Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
This can be solved one level up – once the list of ids is available. But it feels right that a component should be able to handle its data within itself.
Please advice a correct way to immediately update Baobab tree data before the first render of a React Component, from within that Component?
In my case (i am use same stack) wrap branch work fine.
import BaobabPropTypes from 'baobab-react/prop-types';
class Actions {
/**
* #param {Baobab} tree
*/
static prefetchTree = (tree) => {
tree.select(somePath).set(defaultValue);
tree.commit();
};
}
class Page extends React.Component {
static contextTypes = {
tree: BaobabPropTypes.baobab
};
componentWillMount() {
Actions.prefetchTree(this.context.tree);
}
render() {
return <Branch {...this.props}/>;
}
}
Baobab has a get event, use it to detect requests that return values that are not fetched yet:
tree.on('get', function(e) {
if (e.data.data === undefined) {
const path = e.data.path; // requested cursor path like ['data',12345]
const id = path[1];
FETCH_DATA(id)
.then( data => tree.set(path , data) );
tree.set(path, PLACEHOLDER_DATA);
}

Categories