VueJS: Determine the source that triggers the watch callback - javascript

I watch a property of an object device.flow. When the property changes, the callback gets triggered. That all works and is fine.
I only want though that the callback gets triggered if the user actually manipulates the form component that uses that watched property as model.
There's also a periodic API call to get the current data of the device as it might change due to other circumstances than just user input. The slider should then adjust accordingly as well but the web socket event should not be emitted because that is unnecessary.
To illustrate what I want to achieve, I attached a simple component below and added a comment, where I want to distinguish the source of the property change.
Is this even possible?
<script setup lang="ts">
import type DeviceAirValve from "../../../model/DeviceAirValve";
import {reactive, ref, watch} from "vue";
import {useSocketIO} from "../../../plugins/vueSocketIOClient.js";
import type {Socket} from "socket.io-client";
interface Props {
device: DeviceAirValve
}
const props = defineProps<Props>();
const io = useSocketIO() as Socket;
let device = reactive<DeviceAirValve>(props.device);
watch(
() => device.flow,
flow => {
// Determine whether the callback gets called because
// the value was changed by the user through the v-slider component,
// or if the device.flow property has been updated by something else
// (for example a periodic poll from an API) -> isFormInput variable
if (isFormInput) {
flowChangeHandler(flow)
}
}
);
const flowChangeHandler = (newFlow: number): void => {
io.emit('deviceUpdate', {
deviceId: props.device.deviceId,
data: {flow: newFlow}
});
};
</script>
<template>
<v-slider v-model="device.flow"></v-slider>
</template>

One solution is to replace the watcher with an event handler for v-slider's update:modelValue event (the event that drives v-model, which is only triggered by user input):
<v-slider #update:modelValue="flowChangeHandler" />
demo

Related

How to test the reaction to a component event in Svelte?

In Svelte, I have a parent component which listens to a component event dispatched by a child component.
I know how to use component.$on to check that the dispatched event does the right thing within the component which is dispatching, like so.
But I can't figure out how to check that the component which receives the dispatch does the right thing in response.
Here's a basic example:
Child.svelte
<script>
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
function handleSubmit(event) {
dispatch('results', 'some results')
}
</script>
<form on:submit|preventDefault={ handleSubmit }>
<button type='submit'>Submit</button>
</form>
Parent.svelte
<script>
import Child from './Child.svelte'
let showResults = false
function handleResults(event) {
showResults = true
}
</script>
<Child on:results={ handleResults } />
{ #if showResults }
<p id='results'>Some results.</p>
{ /if }
The idea is to eventually write a test using #testing-library/svelte like:
import { render } from '#testing-library/svelte'
import Parent from './Parent.svelte'
test('shows results when it receives them', () => {
const rendered = render(Parent)
// ***
// Simulate the `results` event from the child component?
// ***
// Check that the results appear.
})
If the parent were reacting to a DOM event, I would use fireEvent.
But I don't know how I would get a hold of the <Child> component in this case, and even if I could I'm guessing that Svelte is using a different mechanism for component events.
(Just to test it out, I used createEvent to fire a custom results event on one of the DOM elements rendered by <Child> but it didn't seem to do anything.)
Anyone have any ideas? Thanks!
If you're already planning on using #testing-library/svelte, I think the easiest way is not to try to manually trigger the Child component's results event, but to use Testing Library to grab the form/submit elements and trigger the submit event (using fireEvent a SubmitEvent on the <form> or their #testing-library/user-event library, or even a vanilla dispatchEvent). Svelte would then dispatch the custom results event that Parent is listening on.
Something like:
test('shows results when it receives them', async () => {
// Arrange
const rendered = render(Parent)
const submitButton = rendered.getByRole('button', {
name: /submit/i
});
const user = userEvent.setup();
// Act
await user.click(submitButton);
// Assert
const results = rendered.queryByText(/some results\./i);
expect(results).not.toBe(null);
});
Hope this is what you had in mind.
Edit:
For mocking Child.svelte, something like this in a __mocks__/Child.svelte should work:
<script>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
function handleSubmit(event) {
dispatch("results", "some results");
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<button type="submit">Test</button>
</form>
Which is the exact same implementation as the actual module (I gave the button a different label just to make it clear it's the mocked version when querying it), but the idea is that this would never need to change and is only used to dispatch a results event. Then you'd just need to tell Jest or whatever you're using that you're mocking it (jest.mock("./Child.svelte");), change the getByRole query to match the new name (or just leave the mock with the original name), then it should just work.
Whether you think that's worth it or not is up to you. I've generally had success testing the UI as a whole rather than mocking sub-components, but I guess it comes down to preference. Yes, you might have to change the test if the Child component changes, but only if you change the label of the button or change the user interaction mechanism.
You don't need to know about the details of the components, you don't even need to know that it's split into a separate Child component, all the test would care about is a general idea of the structure of the UIā€”that there's a button called "Submit" and that clicking on it should show an additional <p> tag.

Context value not accessible inside inner function of functional component

I am attempting to build a component that takes in an arbitrarily large list of items and displays a chunk of them at a time. As the user scrolls the window down, I want to automatically load more items if any exist.
The problem I am running into is that the my appState variable is not acting consistently. When I log it at the top of the component, it always reads the correct value out of the loaded context. However, when I read the value inside the onScroll function, it always returns the default uninitialized state. Where did my context go on the inner function?
Here's a stripped down version that illustrates my problem:
Component
import { useContext } from 'react'
import { useLifecycles} from 'react-use'
import AppState from '../../models/AppState'
import { Context } from '../../store/create'
export default () => {
const appState:AppState = useContext(Context)
console.log('appState.items (root)=', appState.items.length) // Returns `100`, as it should
useLifecycles(
() => {
window.addEventListener('scroll', onScroll)
},
() => {
window.removeEventListener('scroll', onScroll)
}
)
const onScroll = (evt:any) => {
console.log('appState.items (onScroll)', appState.items.length) // Returns `0` (the default uninitialized state).
}
return (
<div className='ItemList'>
<h1>Hello world</h1>
{/* The list of items goes here */}
</div>
)
}
../../store/create
import React from 'react'
import AppState, { getDefaultState } from '../models/AppState'
let state:AppState = getDefaultState()
export const Context:React.Context<AppState> = React.createContext<AppState>(state)
export const setAppState = (newState:AppState):void => {
_state = newState
}
export const getAppState = ():AppState => {
return _state
}
I've read the rule of hooks, and to my understanding I am not breaking anything. My useContext and useLifecycle calls are in a fixed order at the top; no conditionals, no loops.
What am I missing?
I am not aware of how useLifecycles work. But the problem I can see is that you are binding the event a function. That function has the state in it's closure and so it captures that value of state. Whenever state changes, your handler isn't aware of the state change and so it just keeps using the data that was previously captured. Now to solve it, you need to listen for state change and remove the listener that was previously attached, add the new listener that has new values in its closure. I think the useLifecycles should have a dependency option to achieve that. If not the other way could be to use useEffect hook.
Edit:
I just checked the react-use docs and turns out what you really need is useEvent. Look at the example in docs. To make sure it works in your case, you should pass your dependency in useCallback.

Invalid hook call React while fetching data from an API

passing country full name at onClick event.
here is error
import React,{useState,useEffect} from 'react'; //
import axios from 'axios';
export const Country = (name) => {
const [country, setCountry] = useState([]);
const requestCountry = (name) => {
axios(`https://restcountries.eu/rest/v2/name/${name}?fullText=true`)
.then((res) =>
// Success handling
setCountry(res.data))
.catch((error) => {
// Error handling
console.error(error.message);
});
}
requestCountry(name)
}
Here is Source of Code Click here to see code
Hooks can be only used inside a Functional Component and not a normal function.
Seems like you are trying to call a Functional Component like normal function with an argument like below.
onClick={() => Country(data.name)}
Instead what you might want to do is, show a list of buttons with country names and then when one of the button is clicked, call a handler function which is the axios API call and then show the response country details or do whatever that you want with those detail data.
To do that, you need to save those responded country detail into a React state.
If country detail exists, show the details. If not, show the list.
So, I forked your codesandbox and edit it like this.
https://codesandbox.io/s/country-data-i479e?file=/src/App.js
Well from the error I can see that you have put the Country call inside a event handler onClick.
The truth is that hooks can not be called inside event listeners. If you need to change the state inside a listener you can do that but you will need to call useState outside of a listener and then call setState wherever you need.
That is because React uses order in which you call hooks to remember how execute your component in subsequent calls.
const [state, setState] = useState();
const onClick = () => {
setState(...);
} ;
As the previous answers have mentioned, you can use hooks only at functional level and not inside a handler.
You just need to move your hook a level up and pass it to your function.
Also, as you're not returning anything from the Country function, there's no need to import "React".
I have modified the code: https://codesandbox.io/s/quiet-night-7cdvi
Check console (Added a useEffect in Country.js just for logging, you can remove it).
Some changed done in your code. Here is the link to view code. https://codesandbox.io/s/practical-dust-lk0x7?file=/src/country.js

How to use a dojo widget inside a react component?

Is there any way to re-use a component/widget from other library inside a react component?
E.g.
export default function App() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>{count}</button>
<button id="btn" onClick={() => function(){
require(["dijit/form/Button", "dojo/domReady!"], function(Button) {
var button = new Button({
label: "Click Me!",
onClick: function(){ console.log("Opening a dojo dialog"); }
}, "btn");
button.startup();
});
}}>Open Dojo component</button>
<br />
</div>
);
Here, this is a basic react component. I am trying to use a dojo button inside it but apparently does not work. I am actually thinking in the lines of having a button event in the react and call some javascript function which parses the dojo widget. Not sure if that will work.
Is there a better approach?
The reason your attempt didn't work is because you're still rendering plain buttons in the React JSX, and when you click that button you're creating a new Dojo button but you're not adding it to the DOM. Even if you did add it to the DOM, React might still remove it on a later re-render because it's not part of the JSX.
We've recently undergone an integration of React into an existing Dojo application, and the method we use to put Dojo widgets into React components is as follows.
The high level overview is that we create special React components that "host" Dojo widgets inside them. I'll start from the top and work my way down.
The usage of these hosted Dojo widgets is mostly straight forward, and looks something like the following JSX
<div>
<Button
dojoProps={{ label: "Add" }}
dojoEvents={{ click: onAdd }}
/>
<Button
dojoProps={{ label: "Remove" }}
dojoEvents={{ click: onRemove }}
/>
</div>
where Button is our Dojo hosting component. The entries in the dojoProps property are passed into the constructor of the widget and into .set(...) when the properties change. Then entries in dojoEvents are passed into .on(...) when being created and when they change as well.
The Button class looks like this (it's in TS, but should be easy to translate into JS)
import * as DojoButton from "dijit/form/Button";
import { DojoInReactComponent } from "../DojoInReactComponent";
export class Button extends DojoInReactComponent<DojoButton> {
constructor(props: Button["props"]) {
super(new DojoButton(props.dojoProps), props);
}
}
We make one of these classes for each Dojo widget we wish to wrap and show in React and is re-usable throughout the project. Note that this is where the widget is created and the props are passed into the widget's constructor.
The important part of the implementation is in the DojoInReactComponent class:
import * as React from "react";
import * as _WidgetBase from "dijit/_WidgetBase";
/**
* A React component that hosts a Dojo widget
*/
export abstract class DojoInReactComponent
<W extends _WidgetBase, P extends DojoInReactComponentProps<W> = DojoInReactComponentProps<W>>
extends React.Component<P> {
/** Stores a React Ref to the actual DOMNode that we place the widget at */
private readonly widgetRef: React.RefObject<HTMLSpanElement>;
/** Cache of the registered event handles for this widget (used to cleanup handles on unmount) */
private readonly eventsRegistered: EventRegistrationCache<W> = {};
/** The actual widget that will be stored in this component */
readonly widget: W;
constructor(widget: W, props: P) {
super(props);
this.widgetRef = React.createRef();
this.widget = widget;
}
componentDidMount() {
if (!this.widgetRef.current) {
throw new Error("this.widgetRef was not set");
}
// First, set properties
this.widget.set(this.props.dojoProps ?? {});
// Then set event handlers. This is the first time it happens so
// the eventsRegistered cache is empty (ie: nothing to remove).
this.addEventHandlers(this.props.dojoEvents);
// Finally, place it at the domNode that this component created when it rendered.
this.widget.placeAt(this.widgetRef.current);
}
componentDidUpdate(prevProps: P) {
// First, update props. Note that we merge the old and new properties together here before setting to
// ensure nothing drastically changes if we don't pass in a property. If we want it to change, we need to
// explicitly pass it in to the dojoProps property in the TSX.
// This also attempts to make it obvious that not setting a property in the TSX will leave it unchanged,
// compared to it's existing value, as that's the default behaviour of Dojo widgets and .set().
const props = { ...prevProps.dojoProps ?? {}, ...this.props.dojoProps ?? {} };
this.widget.set(props);
// Then update event handlers. Note that we don't do this in a "smart" way, but instead we just remove all
// existing handlers, and then re-add the supplied ones. Generally it will mean removing and re-adding the same
// handlers, but it's much easier than trying to diff them.
this.removeAllEventHandlers();
this.addEventHandlers(this.props.dojoEvents);
}
componentWillUnmount() {
// On cleanup we need to remove our handlers that we set up to ensure no memory leaks.
this.removeAllEventHandlers();
// Finally we destroy the widget
this.widget.destroyRecursive();
}
private addEventHandlers(events: EventsType<W> | undefined) {
if (!events) {
return;
}
for (const key of keysOf(events)) {
const newHandle = this.widget.on(key as any, events[key] as any);
this.eventsRegistered[key] = newHandle;
}
}
private removeAllEventHandlers() {
for (const key of keysOf(this.eventsRegistered)) {
const handle = this.eventsRegistered[key];
if (handle) {
handle.remove();
}
delete this.eventsRegistered[key];
}
}
render() {
return <span ref={this.widgetRef}></span>;
}
}
function keysOf<T extends {}>(obj: T) {
return Object.keys(obj) as Array<keyof T>;
}
type EventsType<T extends _WidgetBase> = Record<string, () => void>;
type PropsType<T extends _WidgetBase> = Partial<T>;
export interface DojoInReactComponentProps<W extends _WidgetBase> {
dojoProps?: PropsType<W>;
dojoEvents?: EventsType<W>;
}
type EventRegistrationCache<W extends _WidgetBase> = Record<string, dojo.Handle>;
There's a bit to unpack here, but the general gist of it is:
render() produces a simple <span> element that we keep a reference to to mount our Dojo widget to shortly
componentDidMount() updates the properties and event handlers of the Dojo widget to our supplied properties. It then places the actual Dojo widget into the DOM produced by the React component
componentDidUpdate() again updates the properties and event handlers if they changes due to a re-render of the React component
componentWillUnmount() is called when the React component is being removed and does a teardown of the hosted Dojo widget by calling .destroyRecursive() to remove it from the DOM
The types down the bottom are just utility types to help TS infer everything for us, however EventsType and EventsRegistrationCache uses additional changes that are far too long to write out here, so I've replaced them with a stub (but if you're just using JS then it won't matter).
Let me know if you have any questions about it.

"Remount" a method in vue js component

i'm not so well-versed in Vue.js or frontend so maybe what I'm trying to achieve is not the right approach, but here's my situation:
mounted() {
this.newMessages();
this.scrollDown();
},
updated() {
// prop gets updated from parent component, newMessages method depends on it
this.$options.mounted = [
this.newMessages(),
];
this.scrollDown();
},
methods: {
newMessages() {
// logic, uses prop thread.id passed to component, needs to be initiated on mount
},
scrollDown() {
// logic
},
},
The question is: How to I "remount" a method in Vue.js mounted hook component, if the updated hook fires?
Now, here's what happening, newMessages method get's a broadcast over websockets. It uses a prop called thread (id property of that object) to listen on specific channel.
Now when parent component changes that prop (thread) the DOM of the component changes to show messages from that thread, but that listener method still listens on a wrong channel.
I tried $options to reset mounted hook, but that only stacks the method mounted, and gets the same message multiple times.

Categories

Resources