React setState callback not called for external (npm packaged) component - reactjs

I am using setState with a callback to ensure things run after the state has been updated.
...
this.setState({enabled : true},this.getData);
}
getData () {
const self = this;
fetch(this.props.url,{
method : 'post',
body : this.state
}).then(function (response) {
return response.json();
}).then(function (result) {
self.update();
});
}
...
setState is getting called. this.state.enabled does change to true. However, this.getData is not getting called.
One thing I found interesting is that this is happening for a component I am using via an npm package. In my own code, setState with a callback works as designed.
The said external component is also packaged by me. I am using webpack to build it. Might there be something wrong with my webpack config?
Here it is:
const Webpack = require('webpack');
const path = require('path');
module.exports = {
entry: {
index : './src/index.js'
},
output: {
path: path.join(__dirname,'dist'),
filename: '[name].js',
library : 'TextField',
libraryTarget: 'umd'
},
externals : [
{
'react' : {
root : 'React',
commonjs2 : 'react',
commonjs : 'react',
amd : 'react'
}
}
],
module: {
loaders: [
{ test: /\.(js?)$/, exclude: /node_modules/, loader: require.resolve('babel-loader'), query: {cacheDirectory: true, presets: ['es2015', 'react', 'stage-2']} }
]
},
devtool : 'eval'
};
Edit:
So now I am pretty sure something fishy is going on when I use my component from a package vs, when I use it from my source.
When I call setState from a component which is part of my source-code, this is what is called:
ReactComponent.prototype.setState = function (partialState, callback) {
!(typeof partialState === 'object'
|| typeof partialState === 'function'
|| partialState == null) ?
process.env.NODE_ENV !== 'production' ?
invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.')
: _prodInvariant('85')
: void 0;
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
The above is from a file named ReactBaseClasses.js
When I call setState from a component I packaged as an npm package, this is what is called:
Component.prototype.setState = function (partialState, callback) {
!(typeof partialState === 'object'
|| typeof partialState === 'function'
|| partialState == null) ?
invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.')
: void 0;
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
The above is from a file named react.development.js. Notice that callback is passed to enqueueSetState. Interestingly, when I break into enqueueSetState, the function does nothing with the callback:
enqueueSetState: function (publicInstance, partialState) {
if (process.env.NODE_ENV !== 'production') {
ReactInstrumentation.debugTool.onSetState();
process.env.NODE_ENV !== 'production' ? warning(partialState != null, 'setState(...): You passed an undefined or null state object; ' + 'instead, use forceUpdate().') : void 0;
}
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if (!internalInstance) {
return;
}
var queue = internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
},

This could be a bug with React. Apparently it was a major bug fix to combine the enqueueSetState and enqueueCallback functionality, but these were separately functioning calls in earlier versions of React.
https://github.com/facebook/react/issues/8577
When importing npm modules, sometimes they bring in different versions of React as dependencies, creating these kinds of inconsistency bugs with setState and callbacks.
https://github.com/facebook/react/issues/10320

When you set your state, do it like this.
this.setState({enabled : true}, () => { this.getData() });
The () => {} bind this to it's lexical parent context.

You could (and maybe actually should) call the getData method from componentDidUpdate. It will also happen 100% after the state has been changed and (IMHO) will make your code a little less callback hellish.
Update following comments:
You can compare your previous state/props with the current ones in componentDidUpdate and then call your method if needed:
componentDidUpdate(prevProps, prevState) {
if (prevProps.something !== this.props.something) {
// do some magic
}
}

You mentioned the callback is being called successfully from handleLookupChange, correct?
So handleChange is successfully being called from handleLookupChange after the onEnter event is triggered and is successfully setting the value for state, correct? And that value for state is either
{displayText : self.props.dataSource[selectedIndex]}
or
{displayText : newValue}.
So here is one possibility.
I noticed handleLookupChange takes two values as arguments from onEnter rather than an event. It sounds like you said the onEnter event successfully delivers the two arguments to handleLookupChange and correctly sets state based on them. If not my guess would be handleLookupChange ought to take the event and process it to pass to handleChange.
Or another one.
If that transition is happening successfully, could this be an issue in how values are received from input after the onEnter event?
When you trigger handleLookupChange with onEnter, is that possible that hitting enter with no text in the input somehow returns newValue not as null but as the empty string? This would cause displayText to be set as newValue (as it is not null), thus causing displayText to be set as the empty string, causing the callback addItem's if statement never to trigger and run?
Have you checked that addItem for sure isn't being called above the if statement?
Another thing to check would be whether dataSource at selectedIndex might be picking up the empty string, which could trigger the if statement to not run the inside of addItem.

Related

How to correctly display the stream output of a container in docker desktop extension

As shown here using docker extension API's you can stream the output of a container but when I try to store the data.stdout string for each line of log it simply keep changing like if it's a object reference....I even tried to copy the string using data.stdout.slice() or transforming it in JSON using JSON.stringify(data.stdout) in order to get a new object with different reference but doesn't work :/
...
const[logs,setLogs]=useState<string[]>([]);
...
ddClient.docker.cli.exec('logs', ['-f', data.Id], {
stream: {
onOutput(data): void {
console.log(data.stdout);
setLogs([...logs, data.stdout]);
},
onError(error: unknown): void {
ddClient.desktopUI.toast.error('An error occurred');
console.log(error);
},
onClose(exitCode) {
console.log("onClose with exit code " + exitCode);
},
splitOutputLines: true,
},
});
Docker extension team member here.
I am not a React expert, so I might be wrong in my explanation, but
I think that the issue is linked to how React works.
In your component, the call ddClient.docker.cli.exec('logs', ['-f'], ...) updates the value of the logs state every time some data are streamed from the backend. This update makes the component to re-render and execute everything once again. Thus, you will have many calls to ddClient.docker.cli.exec executed. And it will grow exponentially.
The problem is that, since there are many ddClient.docker.cli.exec calls, there are as many calls to setLogs that update the logs state simultaneously. But spreading the logs value does not guarantee the value is the last set.
You can try the following to move out the Docker Extensions SDK of the picture. It does exactly the same thing: it updates the content state inside the setTimeout callback every 400ms.
Since the setTimeout is ran on every render and never cleared, there will be many updates of content simultaneously.
let counter = 0;
export function App() {
const [content, setContent] = React.useState<string[]>([]);
setTimeout(() => {
counter++;
setContent([...content, "\n " + counter]);
}, 400);
return (<div>{content}</div>);
}
You will see weird results :)
Here is how to do what you want to do:
const ddClient = createDockerDesktopClient();
export function App() {
const [logs, setLogs] = React.useState<string[]>([]);
useEffect(() => {
const listener = ddClient.docker.cli.exec('logs', ['-f', "xxxx"], {
stream: {
onOutput(data): void {
console.log(data.stdout);
setLogs((current) => [...current, data.stdout]);
},
onError(error: unknown): void {
ddClient.desktopUI.toast.error('An error occurred');
console.log(error);
},
onClose(exitCode) {
console.log("onClose with exit code " + exitCode);
},
splitOutputLines: true,
},
});
return () => {
listener.close();
}
}, []);
return (
<>
<h1>Logs</h1>
<div>{logs}</div>
</>
);
}
What happens here is
the call to ddClient.docker.cli.exec is done in an useEffect
the effect returns a function that closes the connection so that when the component has unmounted the connection to the spawned command is closed
the effect takes an empty array as second parameter so that it is called only when the component is mounted (so not at every re-renders)
instead of reading logs a callback is provided to the setLogs function to make sure the latest value contained in data.stdout is added to the freshest version of the state
To try it, make sure you update the array of parameters of the logs command.

React: Component Not Re-rendering After State Update

I'm running into an issue where updating the state, const [currentText, setCurrentText] = useState("");, with the line setCurrentText(script[property].text); does not rerender the component.
I have verified that the state has in fact been updated, but the component just will not re-render with the new values.
function nextScriptLine() {
for (const property in script) {
if (script[property].index === count) {
setCount((count) => {
return count + 1;
});
// console.log(script[property]); correct entry
break;
}
}
for (const property in script) {
if (script[property].index == count) {
setCurrentText(script[property].text);
// console.log(currentText); correct and expented value
break;
}
}
}
<TextBox
input={currentText === "" ? script.introText.text : currentText}
toggleWithSource={toggleWithSource}
nextScriptLine={nextScriptLine}
disableSound={currentText === ""}
/>
Textbox component
export default function TextBox({
input,
toggleWithSource,
nextScriptLine,
disabledSound,
}) {
// useEffect(() => {
// let tempObj = {};
// for (const property in script) {
// if (!property.read) {
// tempObj = script[property];
// setScript({ [property]: { read: true } });
// break;
// }
// }
// }, [script]);
return (
<span className="gameText">
{textHandler(input, toggleWithSource, disabledSound)}
<SubdirectoryArrowLeftIcon onClick={() => nextScriptLine()} />
</span>
);
}
The chrome developer tools, react components viewer, shows correct props and state after calling nextScriptLine():
props
disableSound
:
false
input
:
"Oh dear, you appear to have set yourself on <span class='cast'>fire</span> and died. Let's try that again.\\nTry casting <span class='cast>self fire extend</span>'"
nextScriptLine
:
ƒ nextScriptLine() {}
toggleWithSource
:
ƒ toggleWithSource() {}
new entry
:
I have read through a number of forums, stackoverflow questions/answer, and read multiple medium.com articles on the issue with no success. I have tried useEffect(which causes an infinite loop, which still doesn't have the updated state values anyways), state updater(waiting for state updates etc), and the only thing that seems to work is manually updating state via setCurrentText.
I'm starting to think the typewriter-effect npm package being async is messing with this somehow, because the rest of the state updates and rerenders work fine.
I'm writing this project because I've become rusty after being stagnant from programming for a while, so ANY advice or tips are more than welcome. Thank you in advance!
Update
After commenting out the typewriter component and leaving just return input the components re-render correctly. I would still like to use the typewriter-effect package. Is there anything I can do to get this to work?
Typewrite code:
export default function textHandler(input, toggleWithSource, disableSound) {
<Typewriter
onInit={(typewriter) => {
typewriter
.changeDelay(20)
.typeString(input)
.callFunction(() => {
if (disableSound == false) {
toggleWithSource();
}
alert(input);
})
.start();
}}
/>
}
Tried another typewriter effect package, same issue. I've come to the conclusion that this is something to do with async issues for sure. Due to the lack of documentation and my knowledge, I will probably be moving forward without these packages. As unfortunate as that is, I just don't know how to have the code wait for the typewriter effects to finish I'm assuming.

Spooky Action at a Distance: Electron & React useEffect - Unable to unsubscribe from ipcRenderer events

I have encountered strange behavior when using Electron's ipcRenderer with React's useEffect.
Within my electron app, I have the following code:
import React, { useEffect } from 'react'
const electron = window.require('electron');
const ipcRenderer = electron.ipcRenderer;
...
const someValueThatChanges = props.someValue;
useEffect(() => {
const myEventName = 'some-event-name';
console.log(`Using effect. There are currently ${ipcRenderer.listenerCount(eventName)} listeners.`);
console.log(`Value that has changed: ${someValueThatChanges}.`);
ipcRenderer.addListener(myEventName, myEventHandler);
console.log('Added a new listener.');
// Should clean up the effect (remove the listener) when the effect is called again.
return () => {
ipcRenderer.removeListener(myEventName, myEventHandler)
console.log('Cleaned up event handler.');
}
}, [ someValueThatChanges ]);
function myEventHandler() {
console.log('Handled event');
}
The code above is supposed to listen to the some-event-name event fired by Electron's main process with mainWindow.webContents.send('some-event-name'); and console.log(...) a message inicating that the event was handled.
This works as expected when the effect is initially run. A listener is added, the event is raised at a later time, and the string 'Handled event' is printed to to the console. But when the someValueThatChanges variable is assigned a different value and the event is raised for a second time, the 'Handled event' string is printed out to the console twice (the old listener does not appear to have been removed).
The line with the listenerCount(eventName) call returns 0 as expected when the removeListener(...) call is included in the useEffect return/cleanup function. When the removeListener(...) call is removed, the listenerCount(eventName) call returns a value that is incremented as expected (e.g. 0, 1, 2) as listeners are not removed.
Here's the really weird part. In either case, whether or not I include the call to removeListener(...), the myEventHandler function is always called for as many times as useEffect has been run. In other words, Electron reports that there are no event listeners, but myEventHandler still seems to be called by the previous listeners. Is this a bug in Electron, or am I missing something?
Never try with ipcRenderer.addListener, But try ipcRenderer.on instead
useEffect(() => {
ipcRenderer.send('send-command', 'ping');
ipcRenderer.on('get-command', (event, data) => {
console.log('data', data);
});
return () => {
ipcRenderer.removeAllListeners('get-command');
};
}, []);
I believe, the docs changed. ipcRenderer.removeAllListeners accept single string instead of array of string Source electron issues,

Does passing objects as props interfere with componentWillReceiveProps?

My React app needs to keep track of a configuration object with dynamic keys, so I pass it to a component like this:
<Component configuration={this.state.configuration}>
While this works, when I am in the Component's componentWillReceiveProps(nextProps) I cannot discern configuration changes, because this.props has already been updated to nextProps.
If this isn't a known issue, perhaps it has to do with the way I handle updates to configuration state in the parent? Here's how I update configuration state:
handleConfigurationChangeForKey(newOption, key) {
const configObject = this.state.configuration;
configObject[key] = newOption;
this.setState({configuration: configObject});
}
Thanks in advance for any help.
I cannot discern configuration changes, because this.props has already been updated to nextProps.
This is not true. this.props will have the current props, nextProps the upcoming ones.
The way you set the state could be the problem. Try creating a new configuration object, using Object.create or a deep copying function (such as the one provided by lodash).
const newConfig = Object.create(oldConfig)
# or
const newConfig = _.cloneDeep(oldConfig)
newConfig[key] = newValue
This way, the object won't be equal by reference to the old version. If copying brings a performance problem, you can try the Immutable.js library for your state objects.
When you're updating the config object, you're mutating it: you can't tell the difference between nextProps.configuration and this.props.configuration because they're the same object.
The way to get around this is to basically clone the original config object, mutate that, and then use setState to make configuration point to that new object.
handleConfigurationChangeForKey(newOption, key) {
const nextConfiguration = {
...this.state.configuration,
[key]: newOption
};
this.setState({ configuration: nextConfiguration });
}
Using only older language features
handleConfigurationChangeForKey(newOption, key) {
var nextConfiguration = {};
nextConfiguration[key] = newOption;
nextConfiguration = Object.assign(
{},
this.state.configuration,
nextConfiguration
);
this.setState({ configuration: nextConfiguration });
}

Change state when properties change and first mount on React - Missing function?

I have come across a problem about states based on properties.
The scenario
I have a Component parent which creates passes a property to a child component.
The Child component reacts according to the property received.
In React the "only" proper way to change the state of a component is using the functions componentWillMount or componentDidMount and componentWillReceiveProps as far as I've seen (among others, but let's focus on these ones, because getInitialState is just executed once).
My problem/Question
If I receive a new property from the parent and I want to change the state, only the function componentWillReceiveProps will be executed and will allowed me to execute setState. Render does not allow to setStatus.
What if I want to set the state on the beginning and the time it receives a new property?
So I have to set it on getInitialState or componentWillMount/componentDidMount. Then you have to change the state depending on the properties using componentWillReceiveProps.
This is a problem when your state highly depends from your properties, which is almost always. Which can become silly because you have to repeat the states you want to update according to the new property.
My solution
I have created a new method that it's called on componentWillMount and on componentWillReceiveProps. I have not found any method been called after a property has been updated before render and also the first time the Component is mounted. Then there would not be a need to do this silly workaround.
Anyway, here the question: is not there any better option to update the state when a new property is received or changed?
/*...*/
/**
* To be called before mounted and before updating props
* #param props
*/
prepareComponentState: function (props) {
var usedProps = props || this.props;
//set data on state/template
var currentResponses = this.state.candidatesResponses.filter(function (elem) {
return elem.questionId === usedProps.currentQuestion.id;
});
this.setState({
currentResponses: currentResponses,
activeAnswer: null
});
},
componentWillMount: function () {
this.prepareComponentState();
},
componentWillReceiveProps: function (nextProps) {
this.prepareComponentState(nextProps);
},
/*...*/
I feel a bit stupid, I guess I'm loosing something...
I guess there is another solution to solve this.
And yeah, I already know about this:
https://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html
I've found that this pattern is usually not very necessary. In the general case (not always), I've found that setting state based on changed properties is a bit of an anti-pattern; instead, simply derive the necessary local state at render time.
render: function() {
var currentResponses = this.state.candidatesResponses.filter(function (elem) {
return elem.questionId === this.props.currentQuestion.id;
});
return ...; // use currentResponses instead of this.state.currentResponses
}
However, in some cases, it can make sense to cache this data (e.g. maybe calculating it is prohibitively expensive), or you just need to know when the props are set/changed for some other reason. In that case, I would use basically the pattern you've written in your question.
If you really don't like typing it out, you could formalize this new method as a mixin. For example:
var PropsSetOrChangeMixin = {
componentWillMount: function() {
this.onPropsSetOrChange(this.props);
},
componentWillReceiveProps: function(nextProps) {
this.onPropsSetOrChange(nextProps);
}
};
React.createClass({
mixins: [PropsSetOrChangeMixin],
onPropsSetOrChange: function(props) {
var currentResponses = this.state.candidatesResponses.filter(function (elem) {
return elem.questionId === props.currentQuestion.id;
});
this.setState({
currentResponses: currentResponses,
activeAnswer: null
});
},
// ...
});
Of course, if you're using class-based React components, you'd need to find some alternative solution (e.g. inheritance, or custom JS mixins) since they don't get React-style mixins right now.
(For what it's worth, I think the code is much clearer using the explicit methods; I'd probably write it like this:)
componentWillMount: function () {
this.prepareComponentState(this.props);
},
componentWillReceiveProps: function (nextProps) {
this.prepareComponentState(nextProps);
},
prepareComponentState: function (props) {
//set data on state/template
var currentResponses = this.state.candidatesResponses.filter(function (elem) {
return elem.questionId === props.currentQuestion.id;
});
this.setState({
currentResponses: currentResponses,
activeAnswer: null
});
},

Resources