React and Matchmedia - javascript

I'm using react context API in component did mount to set methods. I'd also like to use the media query there and set a method to open or close the sidenav depending on screen size.
Something like this
componentDidMount() {
let context = this.context;
let path = this.props.pageContext && this.props.pageContext.path;
context.setSidenavLeaf(newPath)
// Below this is where I'd like to use the media query to set the sidenavOPen to false. Just not sure how to achieve that
const match = window.matchMedia(`(max-width: 768px)`)
if(match {
context.setSidenavOpen(false)
}
}
Kind of confused about how to achieve something like this. I want to call the method and set it at a specific media break point in my component did mount. Which is using react router path prop. So if I hit that specific url rendered by that component and the screen size is such, close the sidenav else leave it open.

You need to listen for resize event:
componentDidMount() {
let context = this.context;
let path = this.props.pageContext && this.props.pageContext.path;
context.setSidenavLeaf(newPath);
// Below this is where I'd like to use the media query to set the sidenavOPen to false. Just not sure how to achieve that
this.checkWidth = () => {
const match = window.matchMedia(`(max-width: 768px)`);
if (match) {
context.setSidenavOpen(false);
}
};
this.checkWidth();
window.addEventListener("resize", this.checkWidth);
}
componentWillUnmount() {
window.removeEventListener("resize", this.checkWidth);
}
or add listener to the media query itself:
componentDidMount() {
let context = this.context;
let path = this.props.pageContext && this.props.pageContext.path;
context.setSidenavLeaf(newPath);
// Below this is where I'd like to use the media query to set the sidenavOPen to false. Just not sure how to achieve that
this.match = window.matchMedia(`(max-width: 768px)`);
this.checkWidth = (e) => {
if (e.matches) {
context.setSidenavOpen(false);
}
};
this.checkWidth(this.match);
this.match.addListener(this.checkWidth);
}
componentWillUnmount() {
this.match.removeListener(this.checkWidth);
}

for functional components, there's a hook on github that will do this for you https://www.npmjs.com/package/react-simple-matchmedia

Related

React Function Component use variable (not state)

With React Class Component, I use some variable (not this.state) helping my control logic. Example: this.isPressBackspace = false and when I set variable don't make component re-render (ex: this.isPressBackspace = true).
That's working perfect in Class Component but when I change to Function Component, I dont know where to place this.isPressBackspace.
Here is my example in codesandbox.
https://codesandbox.io/s/function-component-example-3h98d
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
const isPressBackspaceRef = React.useRef(false);
const keyDownPositionRef = React.useRef({});
const onKeyDown = (e) => {
// this is wrong syntax
// this.keyDownPosition OR let keyDownPosition
keyDownPositionRef.current = {
start: e.target.selectionStart,
end: e.target.selectionEnd
};
switch (e.key) {
case "Backspace":
isPressBackspaceRef.current = true; // this is wrong syntax ????
break;
default:
break;
}
};
const onChange = (e) => {
const { end } = keyDownPositionRef;
if (isPressBackspaceRef.current) {
const length = end - e.target.selectionEnd;
alert(`You delete ${length} character`);
}
isPressBackspaceRef.current = false;
};
In my experience you don't use the this keyword when working with function components. Instead you use hooks like useState.
Check the following video for getting started with hooks:
https://www.youtube.com/watch?v=O6P86uwfdR0&ab_channel=WebDevSimplified

how to destroy or block mount events in mounted() vuejs

I'm making a wrapper component so I need to add all the events in mounted() methods. However the thing is, as it's another component, whenever I open that component, event is triggered. I'm not sure how to block it. Even I made it to be triggered when the component is clicked, but it didn't work. It only works for the first mount. After re-open it(from second mount), it just keep triggers all the event and I have to block it.
Is there a way that I can block to not to trigger events in mounted() hook for vuejs?
EDITED:
I'm making leaflet-draw wrapper. all the events are from leaflet-draw doc.
this.addnew() is the one being triggered.
objectLayer.on("layeradd", (e) => {
let layer = e.layer;
layer.on("click", onClickFeatureSelct, layer);
if (typeof layer.options.id === "undefined") {
layer.options.id = L.Util.stamp(layer);
}
if (!layer.feature) {
let json = layer.toGeoJSON();
layer.feature = L.GeoJSON.asFeature(json);
}
let properties = layer.feature.properties;
let keyvalue = L.stamp(layer);
if (layer instanceof L.NodeCircle) {
let latlng = layer.getLatLng();
itemType = "node";
let nodes = this.$store.getters.nodeList;
let result = false;
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].keyvalue == keyvalue) {
result = true;
} else {
result = false;
}
}
if (!result) {
console.log('layer added')
// this.addNew(latlng, itemType, keyvalue);
}
if (!properties.NODE_ID) {
properties.NODE_ID = parseInt(this.newNodeId);
properties.NODE_NAME = "-";
this.addedNodes.push(properties.NODE_ID);
layer.bindTooltip(properties.NODE_NAME + "<br>(" + properties.NODE_ID.toString() + ")");
nodeObj[keyvalue.toString()] = layer;
}
// console.log('added nodes', this.addedNodes)
if (!nodeLayer.hasLayer(layer)) nodeLayer.addLayer(layer);
}
});
Well, As this question got 5 ups, to people who's facing same issue just like me. Here is How I did...
Vue.js mount order when components are related.
Child Component -> Parent Component
Adding this.$nextTick() didn't work.
Even it's a SPA Web application. There is no way to NOT to trigger events when they're in the child component. So I just made it to reload..... I know it's not a good idea to do it but I couldn't find the any other way to fix it. However, I think adding flags to parent component and trigger that event when parent is ready might gonna work.
I will re-try this logic once again and let you know how I've done afterwards. It won't be that soon. Sorry.

How to provide window width to all React components that need it inside my app?

So I've got this hook to return the windowWidth for my App components. I'll call this Option #1.
import {useEffect, useState} from 'react';
function useWindowWidth() {
const [windowWidth,setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWindowWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowWidth;
}
export default useWindowWidth;
And right now I'm basically using it on every component that depends on the window width to render, like:
function Component(props) {
const windowWidth = useWindowWidth();
return(
// RETURN SOMETHING BASED ON WINDOW WIDTH
);
}
And since the hook has an event listener for the resize events, the component stays responsive even after window resizes.
But I'm worried that I'm attaching a new listener for every component that uses that hook and it might slow things down at some point. And I've though of other approach:
Option #2
I use the useWindowWidth() hook only one time, inside a top level component like <App/> and I'll provide the windowWidth value down the chain via context.
Like:
function App() {
const windowWidth = useWindowWidth();
return(
<WindowWidthContext.Provider value={windowWidth}>
<Rest_of_the_app/>
</WindowWidthContext.Provider>
);
}
And then, every component that needs it could get it via:
function Component() {
const windowWidth = useContext(WindowWidthContext);
return(
// SOMETHING BASED ON WINDOW WIDTH
);
}
QUESTION
Am I right in being bothered by that fact that I'm setting up multiple resize listeners with Option #1 ? Is Option #2 a good way to optmize that flow?
If your window with is used by so many components as you mentioned, you must prefer using context. As it reads below:
Context is for global scope of application.
So, #2 is perfect choice here per react.
First approach #1 might be good for components in same hierarchy but only up-to 2-3 levels.
I'm not sure if adding and removing event listeners is a more expensive operation than setting and deleting map keys but maybe the following would optimize it:
const changeTracker = (debounceTime => {
const listeners = new Map();
const add = fn => {
listeners.set(fn, fn);
return () => listeners.delete(fn);
};
let debounceTimeout;
window.addEventListener('resize', () => {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(
() => {
const width=window.innerWidth;
listeners.forEach(l => l(width))
},
debounceTime
);
});
return add;
})(200);
function useWindowWidth() {
const [windowWidth, setWindowWidth] = useState(
() => window.innerWidth
);
useEffect(
() =>//changeTracker returns a remove function
changeTracker((width) =>
setWindowWidth(width)
),
[]
);
return windowWidth;
}
As HMR said in an above thread, my solution was to use redux to hold the width value. With this strategy you only need one listener and you can restrict how often you update with whatever tool you like. You could check if the width value is within the range of a new breakpoint and only update redux when that is true. This only works if your components dont need a steady stream of the window width, in that case just debounce.

How to test react-router-dom?

Problem
I have read https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/testing.md
I want to test react-router-dom, I don't care about how it work, I just need to make sure the library is working into my project boilerplate.
Reproduction
I am testing this component
<Link to="/toto">
toto
</Link>
This is the test
it('it expands when the button is clicked', () => {
const renderedComponent = mount(<Wrapper>
<MemoryRouter initialEntries={['/']}>
<Demo />
</MemoryRouter>
</Wrapper>);
renderedComponent.find('a').simulate('click');
expect(location.pathname).toBe('toto');
});
Expected
to be true
Result
blank
Question
How can I test react-router-dom?
If you look at the code for Link, you see this code:
handleClick = event => {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!this.props.target && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
const { history } = this.context.router;
const { replace, to } = this.props;
if (replace) {
history.replace(to);
} else {
history.push(to);
}
}
};
So, presumably you find Link instead of a and override this method to return a value to your own callback you can validate the path set on the <Link>, This doesn't directly test react-router but it will validate that the paths you have set in your link are correct which is what your tests seem to be validating.
So something like (untested code):
const link = renderedComponent.find(Link)
let result = null
link.handleClick = event => {
const { replace, to } = link.props;
if (replace) {
result = null //we are expecting a push
} else {
result = to
}
}
};
link.simulate('click')
expect(result).toEqual('/toto') // '/toto' or 'toto'?
UPDATE
I've realised that the above doesn't work with a shallow render, however, if you just want to check if the to property is correct, you can probably just do it with expect(link.props.to).toEqual('/toto').

scroll to behavior with react router

Is it possible with react router to maybe specify that Link should scroll to a component instead of rendering it? At the moment my components are rendered all at once (in a slightly long page). I have a nav bar and I would like when a user clicks on a Link in nav to scroll up/down to the appropriate component.
I managed to solve my problem with a little help from this post here.
He's using es6, an es5 version of it will look like this:
const hashLinkScroll = function () {
const {hash} = window.location;
if (hash !== '') {
const milliseconds = 0;
setTimeout(function () { // eslint-disable-line prefer-arrow-callback
const id = hash.replace('#', '');
const element = document.getElementById(id);
if (element) {
element.scrollIntoView();
}
}, milliseconds);
}
}
Watch out if you are using a function declaration rather than a function expression. The latter must be defined before it is called.
As for your router, you will have something like:
<Router history={browserHistory} onUpdate={hashLinkScroll}>your routes go here</Router>

Categories