React: Component Not Re-rendering After State Update - reactjs

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.

Related

How to render and update a list of React components dynamically in a performant way?

I am trying to build a simple form builder, with which you can add questions to a list, and each kind of question has a specific component to create the question, and one to set an answer.
These components are contained in an object like this
{
longText:{
create: (initialData, setNewData)=>{...},
answer: (initialData, setNewData)=>{...}
},
multiple:{
create: (initialData, setNewData)=>{...},
answer: (initialData, setNewData)=>{...}
}
...
}
When a kind of question is selected from a dropdown, the right component should render, showing the initialData.
right now the only way I have found that works is something like this:
{(() => {
return React.createElement(components[questionKindName].create,{
initialData: data,
setNewData: (v) => setTheData(v),
});
})()}
But this has very poor performance,
as I am updating the data, on every change,
and it triggers a new render of the whole list of forms each time.
this is the code that keeps track of the list of questions.
const [questions, setQuestions] = useState<Question[]>([]);
const addNewQuestion = (question: Question) => setQuestions([...questions, question]);
// this is the function invoked to set the new data
const editQuestion = (
questionIndex: Number,
key: keyof Question,
value: string
) =>
setQuestions(
questions.map((s, i) => {
if (s && questionIndex === i) {
s[key] = value;
}
return s;
})
);
how can I make it work nicely?
What is that I am missing here?
Should I put a throttle around the IIFE?
I am learning about lazy and Suspense, might that help?
I know the question could have been clearer,
if you can help, i will be happy to improve it.
Thanks.
PS:
I have tried saving and updating the selected component with a useState and a useEffect hook,
but it just doesn't work.
I always got an error like.. "cannot read "InitialData" on "_ref" as it is undefined" that indicates that it was invoking the function, but not passing the props.

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.

How to render a component inside async function in React

In my mongoDB I have documents with nested objects that corresponds to which make, model and year of the motorbike they fit to.
Example:
fits: {
honda: {
crf250: {
1990: true,
1991: true
},
rx400: {
2000: true
}
},
kawasaki: {
ninja: {
2015: true
}
}
}
I need to loop through all the makes that the document stores in fits field (In the example above it would be honda and kawasaki) and than return all the models that exist under the specific make. I am succesfully receiving the array of all the models under the make in my aggregate method.
return(
<ul style={{listStyleType: 'none'}}>
{Object.keys(props.data.fits).map((make, i) => {
if(db !== null && client !== null){
var query = `fits.${make}`;
var pipeline = [
{
$match: {
[query]: {
'$exists': true,
'$ne': {}
}
},
},
{
$group: {
_id: `$${query}`,
}
}
]
client.auth.loginWithCredential(new AnonymousCredential()).then((user) => {
db.collection('products').aggregate(pipeline).toArray().then((models)=>{
return <Make
style={{border: '1px solid grey'}}
mongoClient={props.mongoClient}
id={props.data._id}
key={i}
make={make}
data={props.data}
_handleDeleteMake={handleDeleteMake}
_updateRowData={props._updateRowData}
_models={models}
>
</Make>
}).catch(e=>console.log(e))
})
}
})}
</ul>
)
However after the call I need to render the makes. It should look something like this:
Next to the orange plus I want to show the list of all the other models that exists under the specific make so I don't have to repeat in writing the model again if its exists already and I can just click on it.
However rendering Make inside the async I am left with blank:
Now from what I understand is that the render finished before the async function finished that is why it simply renders empty list, but I don't really know how should I approach this problem. Any suggestions?
I don't think it's possible for you to render a React element in that async way. When React try to render your element that is inside the ul tags, because you are using async, at the time of DOM painting, there is nothing for React to render. Thus React render blank.
After the async is resolved, React won't re-render because React doesn't know that there is a new element being added in. Thus even when you actually have that element, since React doesn't re-render, you won't see that element in the app
Why does this happen? Because React only re-render when there are certain "signal" that tells React to re-render. Such is state change, props change, hooks call, etc. What you did doesn't fall into any of those categories, so React won't re-render. This is the same reason why you don't directly change the component state and instead must use method like setState to change it.

Mutable global state causing issues with array length

I've been working on a SPA for a while and managing my global state with a custom context API, but it's been causing headaches with undesired rerenders down the tree so I thought I'd give react-easy-state a try. So far it's been great, but I'm starting to run into some issues which I assume has to do with the mutability of the global state, something which was easily solved with the custom context api implementation using a lib like immer.
Here's a simplified version of the issue I'm running into: I have a global state for managing orders. The order object primaryOrder has an array of addons into which additional items are added to the order - the list of available addons is stored in a separate store that is responsible for fetching the list from my API. The orderStore looks something like this:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
}
})
When a user selects to increases the quantity of an addon item, it's added to the addons array if it isn't already present, and if it is the qty prop of the addon is increased. The same logic applies when the quantity is reduced, except if it reaches 0 then the addon is removed from the array. This is done using the following methods on the orderStore:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
},
get orderAddons() {
return orderStore.primaryOrder.addons;
},
increaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
let updatedItem = {
...item,
qty: 1,
};
orderStore.primaryOrder.addons = [
...orderStore.primaryOrder.addons,
updatedItem,
];
} else {
orderStore.primaryOrder.addons[index].qty += 1;
}
console.log(orderStore.primaryOrder.addons);
},
decreaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
return;
} else {
// remove the item from the array if value goes 1->0
if (orderStore.primaryOrder.addons[index].qty === 1) {
console.log("removing item from array");
orderStore.primaryOrder.addons = _remove(
orderStore.primaryOrder.addons,
(i) => i.id !== item.id
);
console.log(orderStore.primaryOrder.addons);
return;
}
orderStore.primaryOrder.addons[index].qty -= 1;
}
}
})
The issue I'm running into has to do with the fact that one of my views consuming the orderStore.addons. My Product component is the consumer in this case:
const Product = (item) => {
const [qty, setQty] = useState(0);
const { id, label, thumbnailUrl, unitCost } = item;
autoEffect(() => {
if (orderStore.orderAddons.length === 0) {
setQty(0);
return;
}
console.log({ addons: orderStore.orderAddons });
let index = orderStore.orderAddons.findIndex((addon) => addon.id === id);
console.log({ index });
if (index !== -1) setQty(orderStore.findAddon(index).qty);
});
const Adder = () => {
return (
<div
className="flex"
style={{ flexDirection: "row", justifyContent: "space-between" }}
>
<div onClick={() => orderStore.decreaseAddonItemQty(item)}>-</div>
<div>{qty}</div>
<div onClick={() => orderStore.increaseAddonItemQty(item)}>+</div>
</div>
);
}
return (
<div>
<div>{label} {unitCost}</div>
<Adder />
</div>
)
}
export default view(Product)
The issue occurs when I call decreaseAddonItemQty and the item is removed from the addons array. The error is thrown in the Product component, stating that Uncaught TypeError: Cannot read property 'id' of undefined due to the fact that the array length reads as 2, despite the fact that the item has been removed ( see image below)
My assumption is that the consumer Product is reading the global store before it's completed updating, though of course I could be wrong.
What is the correct approach to use with react-easy-state to avoid this problem?
Seems like you found an auto batching bug. Just wrap your erroneous mutating code in batch until it is fixed to make it work correctly.
import { batch, store } from '#risingstack/react-easy-state'
const orderStore = store({
decreaseAddonItemQty(item) {
batch(() => {
// put your code here ...
})
}
})
Read the "Reactive renders are batched. Multiple synchronous store mutations won't result in multiple re-renders of the same component." section of the repo readme for more info about batching.
And some insight:
React updates are synchronous (as opposed to Angular and Vue) and Easy State (and all other state managers) use React setState behind the scenes to trigger re-renders. This means they are all synchronous too.
setState usually applies a big update at once while Easy State calls a dummy setState whenever you mutate a store property. This means Easy State would unnecessarily re-render way too often. To prevent this we have a batch method that blocks re-rendering until the whole contained code block is executed. This batch is automatically applied to most task sources so you don't have to worry about it, but if you call some mutating code from some exotic task source it won't be batched automatically.
We don't speak about batch a lot because it will (finally) become obsolete once Concurrent React is released. In the meantime, we are adding auto batching to as many places as possible. In the next update (in a few days) store methods will get auto batching, which will solve your issue.
You may wonder how could the absence of batching mess things up so badly. Older transparent reactivity systems (like MobX 4) would simply render the component a few times unnecessarily but they would work fine. This is because they use getters and setters to intercept get and set operations. Easy State (and MobX 5) however use Proxies which 'see a lot more'. In your case part of your browser's array.splice implementation is implemented in JS and Proxies intercept get/set operations inside array.splice. Probably array.splice is doing an array[2] = undefined before running array.length = 2 (this is just pseudo code of course). Without batching this results in exactly what you see.
I hope this helps and solves your issue until it is fixed (:
Edit: in the short term we plan to add a strict mode which will throw when store data is mutated outside store methods. This - combined with auto store method batching - will be the most complete solution to this issue until Concurrent React arrives.
Edit2: I would love to know why this was not properly batched by the auto-batch logic to cover this case with some tests. Is you repo public by any chance?

Throttling dispatch in redux producing strange behaviour

I have this class:
export default class Search extends Component {
throttle(fn, threshhold, scope) {
var last,
deferTimer;
return function () {
var context = scope || this;
var now = +new Date,
args = arguments;
if (last && now < last + threshhold) {
// hold on to it
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, threshhold);
} else {
last = now;
fn.apply(context, args);
}
}
}
render() {
return (
<div>
<input type='text' ref='input' onChange={this.throttle(this.handleSearch,3000,this)} />
</div>
)
}
handleSearch(e) {
let text = this.refs.input.value;
this.someFunc();
//this.props.onSearch(text)
}
someFunc() {
console.log('hi')
}
}
All this code does it log out hi every 3 seconds - the throttle call wrapping the handleSearch method takes care of this
As soon as I uncomment this line:
this.props.onSearch(text)
the throttle methods stops having an effect and the console just logs out hi every time the key is hit without a pause and also the oSearch function is invoked.
This onSearch method is a prop method passed down from the main app:
<Search onSearch={ text => dispatch(search(text)) } />
the redux dispatch fires off a redux search action which looks like so:
export function searchPerformed(search) {
return {
type: SEARCH_PERFORMED
}
}
I have no idea why this is happening - I'm guessing it's something to do with redux because the issue occurs when handleSearch is calling onSearch, which in turn fires a redux dispatch in the parent component.
The problem is that the first time it executes, it goes to the else, which calls the dispatch function. The reducer probably immediately update some state, and causes a rerender; the re-render causes the input to be created again, with a new 'throttle closure' which again has null 'last' and 'deferTimer' -> going to the else every single time, hence updating immediately.
As Mike noted, just not updating the component can you get the right behavior, if the component doesn't need updating.
In my case, I had a component that needed to poll a server for updates every couple of seconds, until some state-derived prop changed value (e.g. 'pending' vs 'complete').
Every time the new data came in, the component re-rendered, and called the action creator again, and throttling the action creator didn't work.
I was able to solve simply by handing the relevant action creator to setInterval on component mount. Yes, it's a side effect happening on render, but it's easy to reason about, and the actual state changes still go through the dispatcher.
If you want to keep it pure, or your use case is more complicated, check out https://github.com/pirosikick/redux-throttle-actions.
Thanks to luanped who helped me realise the issue here. With that understood I was able to find a simple solution. The search component does not need to update as the input is an uncontrolled component. To stop the cyclical issue I was having I've used shouldComponentUpdate to prevent it from ever re-rendering:
constructor() {
super();
this.handleSearch = _.throttle(this.handleSearch,1000);
}
shouldComponentUpdate() {
return false;
}
I also moved the throttle in to the constructor so there can only ever be once instance of the throttle.
I think this is a good solution, however I am only just starting to learn react so if anyone can point out a problem with this approach it would be welcomed.

Resources