Back in vue 2 i used to call render() like that:
export default {
mounted(){
...
},
render(){
...
},
methods(){
...
}
}
I'm now trying to do the same with Vue 3 and the composition api. Here is what i tried:
export default {
...
setup(props, context){
...
const create_canvas = (h, id, props) => {
_id.value = id
_attrs.value = props.attrs
return () => h('div', {
class: `trading-vue-${id}`,
style: {
left: props.position.x + 'px',
top: props.position.y + 'px',
position: 'absolute',
}
}, [
h('canvas', Object.assign({
id: `${props.tv_id}-${id}-canvas`,
onmousemove: e => renderer.mousemove(e),
onmouseout: e => renderer.mouseout(e),
onmouseup: e => renderer.mouseup(e),
onmousedown: e => renderer.mousedown(e),
ref: 'canvas',
style: props.style,
}, props.attrs))
].concat(props.hs || []))
};
function render() {
const id = props.grid_id
const layout = props.layout.grids[id]
return () => create_canvas(h, `grid-${id}`, {
position: {
x: 0,
y: layout.offset || 0
},
attrs: {
width: layout.width,
height: layout.height,
overflow: 'hidden'
},
style: {
backgroundColor: props.colors.back
},
hs: [
h(Crosshair, Object.assign(
common_props(),
layer_events
)),
h(KeyboardListener, keyboard_events),
h(UxLayer, {
id,
tv_id: props.tv_id,
uxs: uxs.value,
colors: props.colors,
config: props.config,
updater: Math.random(),
onCustomEvent: emit_ux_event
})
].concat(get_overlays(h))
})
};
render()
}
}
This doesn't seem to return anything in my template. I think that i'm not calling the render function in the right way. Can anyone help me understanding how to use it?
As per my understanding, h() is a short form to create the vnodes and accept 3 parameters.
h(
tag name,
props/attributes,
array of children
)
As per my understanding, In create_canvas you are trying to create a div which contains a class and inline styles as a props/attributes and we are creating a canvas as a children of this div vnode. Hence, Instead of returning the vNode directly from setup(), it should return a render function that returns a vNode.
export default {
props: {
// props will come here
},
setup(props) {
// render() { h(...) } ❌
return () => {
h('div', {
class: `trading-vue-${id}`,
style: {
left: props.position.x + 'px',
top: props.position.y + 'px',
position: 'absolute',
}
}, [
h('canvas', Object.assign({
id: `${props.tv_id}-${id}-canvas`,
onmousemove: e => renderer.mousemove(e),
onmouseout: e => renderer.mouseout(e),
onmouseup: e => renderer.mouseup(e),
onmousedown: e => renderer.mousedown(e),
ref: 'canvas',
style: props.style,
}, props.attrs))
].concat(props.hs || []))
} ✅
}
}
First you are are not "calling" render function - you are just declaring it (Vue is caling it when rendering)
In Composition API you just need to return render function from setup
import { ref, h } from 'vue'
export default {
props: {
/* ... */
},
setup(props) {
const count = ref(1)
// return the render function
return () => h('div', props.msg + count.value)
}
}
After applying this knowledge on your own code I would say that the last line of setup should not be render() but return render() (because the the render() function itself returns actual "render" function)
In JS functions are treated as data - you can store them in variables and return them from the functions. When the function is stored or returned as a result of another function, it is not executed immediately - it is only created. The one who calls the "factory" function (in this case factory function is setup() and the caller is Vue) can store the reference to the returned function and decides when to call it
The Vue Composition API onMounted hook works very similarly. You are calling onMounted() passing a newly created function as an argument. onMounted stores the reference to the function somewhere so the Vue can later call it.
The point is that it does not matter that in setup() the onMounted() is executed 1st and your render function is returned as a last statement of setup. Because Vue decides when to call them "sometimes later". And it is reasonable to expect that Vue for sure will call your render function at least once before calling a function passed into onMounted() (because component cannot be mounted before it is rendered)
Related
I have this callback function in which I set some state. The state seems to mutate just fine. However, I cannot refer to the updated state inside the callback function - it always prints out the initial state.
So my question is why this happens and how should I proceed if i want to check on the updated state inside the callback?
import React, { useState, useEffect } from "react";
import Context from "./ctx";
export default props => {
const [state, setState] = useState({
x: 0,
y: 0
});
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`); <<---logs the initial state.
});
return () => {
window.removeEventListener("resize");
};
}, []);
console.log(`${JSON.stringify(state)}`); <<---logs updated versions of the state.
return (
<Context.Provider
value={{
...state
}}
>
{props.children}
</Context.Provider>
);
};
The problem you're facing is related to two things:
How React works
Closures (function + environment in which it was created).
1. How React works
You've created and exported functional React component, which accepts some props, uses hooks for state management, and renders some content.
Whenever some props or state change in your component (or in it's parent component) react will re-render your component, meaning: it will literally call your function, like yourComponent(props).
The point is: the body of your function will be executed every time re-render happens, alongside with calls to useState and useEffect.
2. Closures (function + environment in which it was created)
Whenever we create/define some function in JavaScript it is stored in the runtime memory alongside with the environment in which it was created.
In your case, you're defining the function in question (and providing it as a callback to useEffect at the same time) here:
() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`); <<---logs the initial state.
});
return () => {
window.removeEventListener("resize");
};
}
and the environment in which it's created is the body of your functional component.
So, whenever React calls your component the new environment is created, and new callback function is defined. But, because you provided empty array as second argument (which represents dependencies array docs) here:
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`);
});
return () => {
window.removeEventListener("resize");
};
}, []); // <-- HERE
provided function will be executed only once - on component mount/first render, because you said it doesn't depend on any other value, so there's no need to fire that effect again.
Bottom line: You're defining new callback function on every render, but only first one was actually called. When it was defined, value of state was { x: 0, y: 0 }. That's why you're always getting that value.
Solution
Depending on your needs, you can set dependencies in the second argument, like:
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`);
});
return () => {
window.removeEventListener("resize");
};
}, [state]); // <-- NOW THE CALLBACK WILL BE EXECUTED EVERY TIME STATE CHANGES
or, you can omit second argument altogether, that way it will be fired on every render:
useEffect(() => {
window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log(`${JSON.stringify(state)}`);
});
return () => {
window.removeEventListener("resize");
};
}); // <-- WITHOUT SECOND ARGUMENT
It may seem expensive, but the official React documentation says it shouldn't have any performance hits with event listeners usage, because addEventListener DOM api is very efficient. But if you notice some performance issues, you can always use the first solution :).
Hope it helps!
The reason in javascript closures. useEffect callback has an old version of data because of empty deps array, outside data don't update. So your old version of state object is used. That is why you get the same values
SetState is asynchronous, right after you execute that line of code you will get old values, one thing that you can do is listen for the state to be updated then output the console.log inside that effect, something like:
useEffect(() => {
console.log(`${JSON.stringify(state)}`); //<<---Now it logs the actual state
}, [state.x, state.y]);
Full Example:
import React, { useState, useEffect } from "react";
export const App = props => {
const [state, setState] = useState({
x: 0,
y: 0
});
useEffect(() => {
console.log(`${JSON.stringify(state)}`); //<<--- Now it logs the actual state
}, [state]);
useEffect(() => {
const handler = window.addEventListener("resize", e => {
setState({
x: e.target.window.visualViewport.width,
y: e.target.window.visualViewport.height
});
console.log("updated");
});
return () => {
window.removeEventListener("resize", handler);
};
}, []);
console.log(`${JSON.stringify(state)}`); //<<---logs updated versions of the state.
return (
<div style={{ width: "500px", height: "500px", backgroundColor: "red" }}>
test
</div>
);
};
Codesandbox: https://codesandbox.io/s/unruffled-kowalevski-n87km
At the moment, I learn how to use react context API.
I have a react Provider class, with some state data and functions in the value={}. How can I access a function inside this value from another function inside this value?
So, the function1() is called by a child component. When the state change is finished, I want to call the function2() and access the new state.
Is it possible in react to get something like this?:
class Provider extends Component {
state = {
foo: 'bar'
}
render() {
return() {
<Context.Provider value={{
state: this.state,
function1: () => {
this.setState({ foo: 'changed' }, () => {
// HERE I WANT TO CALL THE NEXT FUNCTION IN THIS VALUE
this.function2()
});
},
function2: () => {
// called by function1 after state change
console.log(this.state)
}
}}>
{ this.props.children }
</Context.Provider>
}
}
}
If I try to run this, and fire the function1() from the child, it gives me
TypeError: _this2.function2() is not a function
I don't understand, why it is trying to access a _this2 variable, because I defined it to access this.function2().
Is it just not possible to do what I want in react? You might say, why the function2() is an extra function, and why I don't add the code in the end of the function1(). It's because the function2() has to be a independent accessable function.
Can anyone help me? Thanks!
What you can try to do is this:-
class Provider extends Component {
state = {
foo: 'bar'
}
an_independent_function() {
console.log(this.state);
}
render() {
return() {
<Context.Provider value={{
state: this.state,
function1: () => {
this.setState({ foo: 'changed' }, () => {
// HERE I WANT TO CALL THE NEXT FUNCTION IN THIS VALUE
this.an_independent_function();
});
},
function2: this.an_independent_function.bind(this)
}}>
{ this.props.children }
</Context.Provider>
}
}
}
I have an important task to do while data is computed within a vuex mapState. I need to call this vue method countAlerts every time data is changed; to do that the computed property needs to call that method but this scope has no vue methods when it is used insight vuex mapState.
export default {
name: "Alerts",
methods: {
countAlerts(data, period) {
/// DO SOMETHING, THEN RETURN DATA
return data;
}
},
computed: {
...mapState({
foundation: state => state.insights.foundation,
insights: state => {
return state.insights.list.filter(al => {
switch (state.insights.foundation.period) {
case "daily":
// ====>> NEED TO CALL METHOD HERE <<=====
al = this.countAlerts(al, "daily");
if (
al.threeDayUp ||
al.threeDayDown ||
al.greatDayUp ||
al.greatDayDown
) {
return al;
}
break;
/// MORE CODE ABOVE
}
});
}
})
}
};
this is bound to the component's contex when you define computed props as functions.
From the docs:
// to access local state with `this`, a normal function must be used
countPlusLocalState (state) {
return state.count + this.localCount
}
Grid view is not being shown with space, rows and column wise and When I am clicking delete menu item, it is passing the last array value (last card's value) to the function, not the clicked card's value. Something is wrong in Grid view.
Following is the data used by the cards. Import statements are there.
Array:
0: {id: "5", title: "Java", price: "78$"}
1: {id: "2", title: "C++", price: "79$"}
2: {id: "4", title: "C", price: "127$"}
3: {id: "1", title: ".Net", price: "65$"}
4: {id: "3", title: "React Js", price: "67$"}
This is the code of my component:
const styles = theme => ({
root: {
flexGrow: 1,
},
paper: {
padding: theme.spacing.unit * 2,
textAlign: 'center',
color: theme.palette.text.secondary,
},
card: {
maxWidth: 400,
},
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
actions: {
display: 'flex',
},
});
const ITEM_HEIGHT = 40;
class Products extends Component {
constructor() {
super();
this.state = {
products: [],
searchString: ''
};
this.getProducts()
}
state = {
anchorEl: null,
};
handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ anchorEl: null });
};
delete = id => {
alert(id)
axios.post('http://localhost:9022/products/delete/' + id)
.then(res => {
let updatedProducts = [...this.state.products].filter(i => i.id !== id);
this.setState({ products: updatedProducts });
});
}
getProducts() {
axios.get('http://localhost:9022/products/getAll')
.then(res => {
this.setState({ products: res.data });
console.log(this.state.products);
});
}
onSearchInputChange = (event) => {
if (event.target.value) {
this.setState({ searchString: event.target.value })
} else {
this.setState({ searchString: '' })
}
this.getProducts()
}
render() {
const { anchorEl } = this.state;
const open = Boolean(anchorEl);
const { classes } = this.props;
return (
<div>
<TextField style={{ padding: 24 }}
id="searchInput"
placeholder="Search for products"
margin="normal"
onChange={this.onSearchInputChange} />
<Grid container spacing={12}>
<Grid item xs={4} xm={4}>
<div className="row">
{this.state.products.map(currentProduct => (
<div key={currentProduct.id}>
<Card>
<CardHeader
action={
<IconButton aria-label="More"
aria-owns={open ? 'long-menu' : null}
aria-haspopup="true"
onClick={this.handleClick}>
<MoreVertIcon />
<Menu
id="long-menu"
anchorEl={anchorEl}
open={open}
onClose={this.handleClose}
PaperProps={{
style: {
maxHeight: ITEM_HEIGHT * 4.5,
width: 100,
},
}}
>
<MenuItem component={Link} to={'/products/' + currentProduct.id}>Edit
</MenuItem>
<MenuItem onClick={() => this.delete(currentProduct.id)}>Delete
</MenuItem>
</Menu>
</IconButton>
}
title={currentProduct.title}
/>
<CardContent>
<Typography component="p">
{currentProduct.id}
</Typography>
</CardContent>
</Card>
</div>
))}
</div>
</Grid>
</Grid>
</div>
)
}
}
export default withStyles(styles)(Products);
I see what the problem is. It's a problem with your code logic.
What you're trying to do in the action section of the CardHeader is rendering a Menu that has two static items in it
<MenuItem component={Link} to={'/products/' + currentProduct.id}>Edit</MenuItem> <MenuItem onClick={() => this.delete(currentProduct.id)}>Delete</MenuItem>
The thing is the Menu has to have a unique id, but each time you render it you give the same => simple-menu instead you could do something like simple-menu-${currentProduct.id}. And the best to do is that you render a separate component from the CardHeader instead of actions.
This gives you more control over you component and each element you want to render.
See and edit it here:
I personally don't like to put a Menu inside of that card, instead I'd put icons to the top left/right of the card.
Uncomment the action property in the CardHeader and comment out the component one to see what I mean!
I hope that's clear, let me know if it isn't!
There are some things that you can fix which should solve all your problems, or at least guide you through the right direction.
I will guide you through, but it would be ideal if I you read the docs first.
You are calling this.getProducts() inside your constructor, and that function uses setState
You are initialising and setting anchorEl in the state outside of the constructor
You are calling super without passing on props, which might lead to bugs
You are not binding functions that use this (handleClick, handleClose, getProducts, etc), which could lead to undefined state of this.
You are calling functions that get values from the state right after calling setState, which might not get the correct values because of how setState works in React.
You should avoid all.
Constructor, bindings, first time fetch
Constructor
Constructor from the official docs:
You should not call setState() in the constructor(). Instead, if your
component needs to use local state, assign the initial state to
this.state directly in the constructor:
The constructor for a React component is called before it is mounted.
When implementing the constructor for a React.Component subclass, you
should call super(props) before any other statement. Otherwise,
this.props will be undefined in the constructor, which can lead to
bugs.
Typically, in React constructors are only used for two purposes:
Initializing local state by assigning an object to this.state.
Binding event handler methods to an instance.
Your code:
constructor() {
super();
this.state = {
products: [],
searchString: ''
};
this.getProducts()
}
state = {
anchorEl: null,
};
Change it to:
constructor(props) {
super(props);
this.state = {
products: [],
searchString: '',
anchorEl: null,
};
this.onSearchInputChange = this.onSearchInputChange .bind(this);
this.getProducts = this.getProducts.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleClose = this.handleClose.bind(this);
}
First time fetch
To call this.getProducts() when the app starts, don't use the constructor, use componentDidMount instead.
componentDidMount from the official docs:
componentDidMount() is invoked immediately after a component is
mounted (inserted into the tree). Initialization that requires DOM
nodes should go here. If you need to load data from a remote endpoint,
this is a good place to instantiate the network request.
Create this function inside the component:
componentDidMount(){
this.getProducts();
}
Bindings
Binding from the official docs:
There are several ways to make sure functions have access to component
attributes like this.props and this.state, depending on which syntax
and build steps you are using
- Bind in Constructor (ES2015)
- Class Properties (Stage 3 Proposal)
- Bind in Render
You can use any of these, but I suggest you use the first one.
Your functions:
handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ anchorEl: null });
};
Change them to:
handleClick(event) {
this.setState({ anchorEl: event.currentTarget });
};
handleClose() {
this.setState({ anchorEl: null });
};
setState
Correct use of setState
setState from the official docs
setState(updater[, callback])
setState() enqueues changes to the component state and tells React
that this component and its children need to be re-rendered with the
updated state. This is the primary method you use to update the user
interface in response to event handlers and server responses.
setState() does not always immediately update the component. It may
batch or defer the update until later. This makes reading this.state
right after calling setState() a potential pitfall. Instead, use
componentDidUpdate or a setState callback (setState(updater,
callback)), either of which are guaranteed to fire after the update
has been applied. If you need to set the state based on the previous
state, read about the updater argument below.
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
So you should not do this:
onSearchInputChange = (event) => {
if (event.target.value) {
this.setState({ searchString: event.target.value })
} else {
this.setState({ searchString: '' })
}
this.getProducts()
}
Because you cannot guarantee that when this.getProducts() is called, the previous setState functions have finished. This means that it might work most of the times, but there would be some cases when React hasn't finished updating the state and you are already calling this.getProducts().
Instead, you should call this.getProducts() once setState has finished, and to guarantee that just use the callback like this (and I am also changing the function's declaration because we already bound it in the constructor with the previous change):
onSearchInputChange(event) {
let newSearchString = '';
if (event.target.value) {
newSearchString = event.target.value;
}
// call getProducts once React has finished updating the state using the callback (second argument)
this.setState({ searchString: newSearchString }, () => {
this.getProducts();
});
}
Your getProducts is OK (now that we bound it in the constructor), but you are calling console.log when it should not be called:
getProducts() {
axios.get('http://localhost:9022/products/getAll')
.then(res => {
this.setState({ products: res.data });
console.log(this.state.products);
});
}
Based on the previous explanation of setState, call it like this:
getProducts() {
axios.get('http://localhost:9022/products/getAll')
.then(res => {
this.setState({ products: res.data }, () => {
console.log(this.state.products);
});
});
}
Your delete function
Assuming that your data is in fact an array, like this:
products: [
{id: "5", title: "Java", price: "78$"}
{id: "2", title: "C++", price: "79$"}
{id: "4", title: "C", price: "127$"}
{id: "1", title: ".Net", price: "65$"}
{id: "3", title: "React Js", price: "67$"}
]
the code that you have should work with the previous changes in the component. But, there is something you can improve as well.
This is your code:
delete = id => {
alert(id)
axios.post('http://localhost:9022/products/delete/' + id)
.then(res => {
let updatedProducts = [...this.state.products].filter(i => i.id !== id);
this.setState({ products: updatedProducts });
});
}
I will refer back to the docs to the setState documentation where the updater function is explained:
setState(updater[, callback])
The first argument is an updater function with the signature:
(state, props) => stateChange
state is a reference to the component state at the time the change is
being applied. It should not be directly mutated. Instead, changes
should be represented by building a new object based on the input from
state and props. For instance, suppose we wanted to increment a value
in state by props.step:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
Both state and props received by the updater function
are guaranteed to be up-to-date. The output of the updater is
shallowly merged with state.
It's important to understand when to use this updater function, and the state param in that function.
The easiest case is the one they mention:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
This could be done like this:
this.setState({counter: this.state.counter + this.props.step});
But since you cannot guarantee that setState has been successful and that it has finished updating the values, you should do it using the updater function.
Now, back to your delete function.
Change this:
delete = id => {
alert(id)
axios.post('http://localhost:9022/products/delete/' + id)
.then(res => {
let updatedProducts = [...this.state.products].filter(i => i.id !== id);
this.setState({ products: updatedProducts });
});
}
to this (notice that I changed the parameter name state to prevState in the updater function so that it makes more sense and it's easier to understand):
delete = id => {
alert(id);
axios.post('http://localhost:9022/products/delete/' + id)
.then(res => {
// To guarantee you get the correct values, get them from the state in the updater function in setState
this.setState((prevState, prevProps) => {
// This happens inside the setState function
let updatedProducts = [...prevState.products].filter(i => i.id !== id);
// The updater function must return the values that will be modified in the state
return ({
products: updatedProducts
});
});
});
}
It's important to notice that filtering before setState like this:
let updatedProducts = [...this.state.products].filter(i => i.id !== id);
this.setState({ products: updatedProducts });
Will work most of the times but it is not recommended, use the updater function instead when handling this situations to ensure everything works every time.
When we translated the previous code to ES2015 syntax, some functions got converted to a different syntax. Some of them are funcName() and some of them are funcName = () =>. What's the difference?
import React, { Component, PropTypes } from 'react';
export default class Stopwatch extends Component {
state = {
running: false,
previouseTime: 0,
elapsedTime: 0,
}
componentDidMount() {
this.interval = setInterval(this.onTick);
}
componentWillUnmount() {
clearInterval(this.interval);
}
onStart = () => {
this.setState({
running: true,
previousTime: Date.now(),
});
}
onStop = () => {
this.setState({
running: false,
});
}
onReset = () => {
this.setState({
elapsedTime: 0,
previousTime: Date.now(),
});
}
onTick = () => {
if (this.state.running) {
var now = Date.now();
this.setState({
elapsedTime: this.state.elapsedTime + (now - this.state.previousTime),
previousTime: Date.now(),
});
}
}
render() {
var seconds = Math.floor(this.state.elapsedTime / 1000);
return (
<div className="stopwatch" >
<h2>Stopwatch</h2>
<div className="stopwatch-time"> {seconds} </div>
{ this.state.running ?
<button onClick={this.onStop}>Stop</button>
:
<button onClick={this.onStart}>Start</button>
}
<button onClick={this.onReset}>Reset</button>
</div>
)
}
}
funcName() in a JavaScript class will add a function to the prototype. These functions will exist only once, attached to the prototype. Without the class syntax you would define a prototype function as follows:
Stopwatch.prototype.funcName = function() { /* ... */ };
The other functions are actually created as properties that contain anonymous functions that exist once per each instance and are created in the constructor when the class is instantiated. This is equivalent to creating them in the constructor as below:
class Stopwatch /* ... */ {
constructor() {
this.onStart = () => { /* ... */ };
}
}
The reason for doing this is that using the => syntax to create the functions causes the functions to be permanently bound to the this value of each instance. Therefore they can be passed around to other things (such as set as event handlers) and when they are called, this will always point to the correct object inside the function.
Whether this is a good idea or just plain unreadable/too tricky is perhaps a matter of taste.