Is there a way to tell Svelte not to recycle a component? - javascript

I don't know if the title is super accurate, couldn't think of a better way to explain it.
Let's say we have a model:
let model = [
{
"id": 0,
"title": "Apple"
},
{
"id": 1,
"title": "Orange"
},
{
"id": 2,
"title": "Tomato"
},
{
"id": 3,
"title": "Avocado"
}
]
That we render like this:
{#each model as item}
<Item {...item}></Item>
{/each}
In the Item component we print to console when the component gets mounted and destroyed:
<script>
import {onMount, onDestroy} from 'svelte'
export let id
export let title
onMount(() => {
console.log('Mounted: ' + title)
})
onDestroy(() => {
console.log('Destroyed: ' + title)
})
</script>
<div id={id}>{title}</div>
We also have a button, which deletes the "Orange" element from the model:
function deleteOrange() {
model.splice(1, 1)
model = model // I don't know if this is the correct way to force an update
}
// ...
<input on:click={deleteOrange} type="button" value="Delete Orange">
When the button is pressed, the Orange component disappears from the DOM as it should, but the console says:
"Destroyed: Avocado"
Is there a way to tell Svelte to destroy the "Orange" component, instead of destroying "Avocado" and moving props around to match the model?
Svelte REPL:
https://svelte.dev/repl/f22b37cb88c449118ff76bb5a82c296e?version=3.48.0

Simply add a (key) to your each loop:
{#each model as item (item.id)}
<Item {...item}></Item>
{/each}
This will ensure that DOM elements are properly tracked. As per the Svelte documentation:
If a key expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.

Related

Is it possible to send Vue/Javascript code within an object to another component?

I have a parent and child Vue components. The child component supplies the parent component with data required to render the page using a simple object that is emitted using emit.
Child component data:
const Steps [
{
sequence: 1,
name: "Personal",
description: `<p>Enter your name and phone number</p>`,
},
{
sequence: 2,
name: "OTP",
description: `<p>An OTP code has been sent to you. Resend code</p>`,
},
]
const SelectedStep = ref( 0 ); // Which step did you select?
const ActiveStep = ref( {} ); // What step is the form currently on?
SelectedStep.value += 1; // e.g. SelectedStep.value === 2
// Get the object in Steps array where the sequence === 2
ActiveStep.value = Steps.find(step => {
return step.sequence === SelectedStep.value
})
// Send this to the parent to render the description and title
emit('SelectedStep', ActiveStep.value);
Depending on which sequence is selected, the object within Steps matching that sequence value will get loaded into ActiveStep. This is then emitted/supplied to the parent component.
However if you look at the object with sequence: 2 above, within the description is a resend code text. I need that to be a link with a binding so that when it is clicked a function is run to resend the code. I imagined something like this:
{
sequence: 2,
name: "OTP",
description: `<p>An OTP code has been sent to you. <a v-on:click="resendOTP">Resend code</a></p>`,
},
When that is rendered on the page, the v-on:click is not being interpreted and is rendered as-is in the HTML.
The parent component is just a view that uses this component:
<header>
<h1>{{ActiveStep.title}}</h1>
<div v-html="`${ActiveStep.description}`">{{ActiveStep.description}}</div>
</header>
<div>
<div class="content">
<Component-Signup v-on:SelectedStep="updateActiveStep"/>
</div>
</div>
<script>
import ComponentSignup from "../../components/Signup.vue"
export default {
components: {
"Component-Signup": ComponentSignup
},
setup() {
const ActiveStep = ref({});
function updateActiveStep(SelectedStep) {
ActiveStep.value = SelectedStep // SelectedStep is the object emitted from child component
}
return {
updateActiveStep,
ActiveStep
}
}
}
</script>
How could this be achieved?
First, your description contains HTML, so interpolation ({{ }}) will not display it as you expect ...it will be displayed encoded
v-html directive can be used to render raw HTML
BUT v-html is useful ONLY for HTML. Any Vue related functionality (as v-on) will not work. Docs:
Note that you cannot use v-html to compose template partials, because Vue is not a string-based templating engine. Instead, components are preferred as the fundamental unit for UI reuse and composition.
Your only option is to create separate component for each step, and use is to display the right component for the current step...

How can I print all elements of an array (passed as props from the parent's state) inside JSX code?

I am new to React and I am trying to navigate the JSX syntax, combined with state/ props and pure Javascript.
I am passing props from the parent component (App) to the child component (Character). One of the props is an array of numbers which I am trying to print to the page, inside a paragraph, when the child component gets rendered. I figured it out how to print all other props (which are either strings or numbers) but I can't find a solution for the seasons array. I can print all the elements of the array to the console using a forEach loop but I have no idea how to integrate the result into JSX. I would like for the last paragraph of the child component to read Seasons: 1, 2, 3 or how many numbers the array has.
EDIT: I would like for each printed season to be a separate element (a link for example) and not to transform the array into a single string which reads "1, 2, 3" (like when using join(,).
Here is my code for the App component:
import React, { Component } from 'react';
import './App.css';
import Characters from './Characters'
class App extends Component {
constructor(props) {
super(props);
this.state = {
characters: [
{
screenName: "Uhtred Uhtredson",
description: "Uthred's younger son, originally named Osbert.",
realName: "Alexander Dreymon",
seasons: [1, 2, 3],
id: 1
},
{
screenName: "Brida",
description: "Former lover of Uhtred. Ally to Ragnall Ivarsson. Lady of Dunholme and widow to Ragnar.",
realName: "Emily Cox",
seasons: [1, 2, 3],
id: 2
},
{
screenName: "King Edward",
description: "King Alfred's son and King of Wessex.",
realName: "Timothy Innes",
seasons: [1, 2, 3],
id: 3
}
]
}
}
render() {
return (
<div className="App" >
<Characters theCharacters={this.state.characters} />
</div>
);
}
}
export default App;
And this is the code for the Character component:
import React from 'react';
const Characters = (props) => {
// we use destructuring by storing the array of objects into the characters const
const { theCharacters } = props;
const charactersList = theCharacters.map(character => {
return (
<div className="character" key={character.id}>
<p>On screen name: {character.screenName}</p>
<p>Short description: {character.description}</p>
<p>Real name: {character.realName}</p>
<p>Apparitions: {
character.seasons.forEach(season => {
return season; //this here doesn't do anything
//console.log(season);
})}</p>
</div>
)
})
return (
<div className="character_list">
{charactersList}
</div>
)
}
export default Characters;
Thank you!
character.seasons.join(',') it takes array elements and returns string, comma is a separator
forEach doesn't return anything. You should use map.
{
character.seasons.map(season => <p>{season}</p>)
}
If you render an array of JSX elements, they'll render just fine; so you can just map the array of values to an array of JSX elements. Note that in these cases, it's important for each element to have a unique key that cannot change for that element, even if it moves positions in the array, to help React render them properly. Luckily, you already have an ID for each character you can use :)
So in your case, you can do something like this:
return (
<div className="character_list">
{charactersList.map(character => (
<span key={character.id}>{character.screenName}</span>
))}
</div>
)
And of course you can render more than just the screenName however you want in there.

Angular re-renders list of components when state is changed without mutation

I am coming from React and I've been working with Angular for a few months now. A React component updates only the necessary HTML when is re-rendered, while an Angular component seems to re-render completely when the state is not mutated. If I mutate the state directly seems to work just fine.
Here's my very basic example:
I have the main component which holds the state:
items = [{ name: "John", age: 8 }, { name: "Jane", age: 20 }];
and in its view it renders a list of items:
<item *ngFor="let item of items" [item]="item"></item>
the item component looks like this:
#Component({
selector: "item",
template: `
<p>
{{ item.name }}, {{ item.age }}
<input type="checkbox" />
</p>
`
})
export class ItemComponent {
#Input() item: any;
}
I added that checkbox in the item component just to notice when the component is fully re-rendered.
So what I do is tick the checkboxes and increment the age for each user in the state.
If I increment it by mutating the state the view is updated as expected and the checkboxes remain checked:
this.items.forEach(item => {
item.age++;
});
But if I change the age without directly mutating the state the entire list is re-rendered and the checkbox is not checked anymore:
this.items = this.items.map(item => ({
...item,
age: item.age + 1
}));
Here's a full example in a CodeSandbox.
Can anyone please explain why is this happening and how can I make the list to not fully re-render when I don't mutate the state?
NgForOf directive which Angular uses to render list of items checks if there are any changes in array we pass to it.
If you mutate property in items then Angular knows that there is no changes since references to objects in array remain the same.
On the other hand, if you completely replace the previous object with new one then it is signal for ngForOf to rerender that object.
To determine if an object changes ngForOf directive use DefaultIterableDiffer that looks at trackByFn function if we provided it. If we don't provide it then the default function looks like:
var trackByIdentity = function (index, item) {
return item;
};
And then Angular compares result of this function with previous value in collection.
You can think about trackByFn like key in React.
So your solution might look like:
*ngFor="let item of items; trackBy: trackByFn"
trackByFn(i) {
return i;
}
https://codesandbox.io/s/angular-67vo6

Manipulating data before rendering in React component

Coming with an Angular background to React, I can't wrap my head around on how to manipulate and restructure data before rendering it.
Let's say I pass this object from parent component to child:
{
"_id": "5c716c53591610007f6d44ef",
"model": {
"_id": "5c7166eb591610007f6d44d4",
"name": "E 300"
},
"serie": {
"_id": "5c716ba0591610007f6d44e2",
"name": "E-Class"
}
},
{
"_id": "5c716c60591610007f6d44f2",
"model": {
"_id": "5c7166f2591610007f6d44d6",
"name": "E 220"
},
"serie": null
},
{
"_id": "5c716c6a591610007f6d44f3",
"model": {
"_id": "5c7166fe591610007f6d44d8",
"name": "C 180"
},
"serie": {
"_id": "5c716ba4591610007f6d44e3",
"name": "C-Class"
}
},
{
"_id": "5c716c6e591610007f6d44f4",
"model": {
"_id": "5c716702591610007f6d44d9",
"name": "C 200"
},
"serie": {
"_id": "5c716ba4591610007f6d44e3",
"name": "C-Class"
}
},
{
"_id": "5c716c74591610007f6d44f5",
"model": {
"_id": "5c716705591610007f6d44da",
"name": "C 220"
},
"serie": {
"_id": "5c716ba4591610007f6d44e3",
"name": "C-Class"
}
}
I want to categorise each model name under their series.
For example C-Class: ['E300', 'E220'] etc, and also put models with no series defined into an object NoClass.
It's too much for an inline jsx (at least it seems so ??) so I need some helper functions, but I can't manage to do the manipulation because every time I get error that the data is undefined which means it tries to render code before it even appears there and gets modified.
So pretty much I want to filter data into new objects and then render these instead of the original props data. And I don't know how to do that all before rendering
My current attempt at doing it inline - it does render however it doesn't check for empty series objects and breaks when where are more than 1 different serie:
class ModelSelect extends React.Component {
render() {
const { models } = this.props
return (
models && <div className={"ModelSelect_Wrapper"}>
<Select placeholder={"Model"} loading={models.loading} disabled={models.data.length === 0}>
{_.uniqBy(models.data, 'name').map((serie) =>
<OptGroup label={serie.serie.name}>
{!models.loading && models.data.map((model, index) =>
<Option value={model.model._id} key={index}>{model.model.name}</Option>
)}
</OptGroup>
)}
</Select>
</div>
);
}
The easiest way to do this is using stores. You have several options available, such as Redux, Flux, and Mobx. Unfortunately, learning to use Stores is a bit hard. coming from angular, i would compare a store to $scope from angular.js 1x, with the difference being that there is no magic causing the view to rerender, you have to rely on react detecting a change in the props in order for rerender to occur. the idea is that the store is a data structure, and when it is modified, the components which are using the store as a prop will be rerendered. this works great especially if you are fetching data async.
the basic pattern would be
use componentDidMount() to load the data in Higher Order Component
transform the data and update the store with the new data
you are passing the store to your component like <MyComponent store={store} />
when data is loaded, you pass the store to the child component
doing it this way your component may look something like
class MyComponent extends React.Component {
static defaultProps = {
store:{}
}
state = {
loading: true
}
componentDidMount() {
this.props.store.loadSomeData().then(()=>this.setState({loading:false}));
}
render(){
return this.state.loading ? null : <MyChildComponent store={store} />
}
}
please remember, you are responsible for loading and transforming the data in your store, and this is just a generalized overview of how your component show load and receive that transformed data. your exact approach will vary wildly based upon the store you are using.
My answer can't be any more specific than this, because there is far too much to cover, and the scope of the work required varies highly depending on the library you use. For example, mobx-3x all you do is decorate your component with #inject and #observer and it handles the shouldComponentUpdate() implementation for you.
So your, first step is to pick a store, and then go from there. best of luck.

How to manage state in a tree component in reactjs

I've been struggling this for a couple of days, trying to figure out the "react" way to do it.
Basically, I have a tree, a list of lists (of lists ...) that can be arbitrarily nested, and I want a component that will display this and also enable rearrangement.
Here's my data:
var data = [{
id: 1
}, {
id: 2, children: [
{
id: 3, children: [{id: 6}]
}, {
id: 4
}, {
id: 5
}]
}]
My first pass was to just have a single "tree" component that builds the nested lists of DOM elements in its render function (look at the code here). That actually worked pretty well for small numbers of elements, but I want to be able to support hundreds of elements, and there was a very high re-render cost when an element was moved within the tree (~600ms when there were a few hundred elements).
So I think I'll have each "node" of the tree be it's own instance of this component. But here's my question (sorry for the long intro):
Should each node dynamically query for the list it's children's IDs from a central "database" and store that in state? Or should the top-most node load the whole tree and pass everything down through props?
I'm still trying to wrap my mind around how state & props should be handled & divvied up.
Thanks
I wanted to try out the tree structure with React and came up with a simple component that hides subtrees when you click on <h5>. Everything is a TreeNode. Is this similar to what you were thinking?
You can see it in action in this JSFiddle: http://jsfiddle.net/ssorallen/XX8mw/
TreeNode.jsx:
var TreeNode = React.createClass({
getInitialState: function() {
return {
visible: true
};
},
render: function() {
var childNodes;
if (this.props.node.childNodes != null) {
childNodes = this.props.node.childNodes.map(function(node, index) {
return <li key={index}><TreeNode node={node} /></li>
});
}
var style = {};
if (!this.state.visible) {
style.display = "none";
}
return (
<div>
<h5 onClick={this.toggle}>
{this.props.node.title}
</h5>
<ul style={style}>
{childNodes}
</ul>
</div>
);
},
toggle: function() {
this.setState({visible: !this.state.visible});
}
});
bootstrap.jsx:
var tree = {
title: "howdy",
childNodes: [
{title: "bobby"},
{title: "suzie", childNodes: [
{title: "puppy", childNodes: [
{title: "dog house"}
]},
{title: "cherry tree"}
]}
]
};
React.render(
<TreeNode node={tree} />,
document.getElementById("tree")
);
Seems like it'd be nicer to pass everything down as props, as this will prevent you from the trouble of managing individual insertion/deletion. Also, like the comments said, the key attributes prevents a huge chunk of unnecessary re-rendering.
You might want to check this link: http://facebook.github.io/react/blog/2013/11/05/thinking-in-react.html. It describes the kind of dilemma you're having and how to approach it.
(Coincidentally, I've made a react tree view a while ago: https://github.com/chenglou/react-treeview. Take whatever you want from it!)
Here is a quick example of how to create a treeview using React and Flux.
http://www.syntaxsuccess.com/viewarticle/5510d81be1ce52d00e93da55
The React component is recursive and state is managed using Flux.

Categories