I need to show a dialog only on small screens. After researching I found out that there is a hook called useMediaQuery() which returns a boolean if the browser meets a specific resolution.
I'm using "#mui/material": "^5.1.1"
This is my implementation:
import { useTheme, useMediaQuery, Dialog, DialogTitle } from '#mui/material';
import { useEffect, useState } from 'react';
const MyDialog = () => {
const theme = useTheme();
const [dialogOpen, setDialogOpen] = useState(false);
useEffect(() => {
setDialogOpen(useMediaQuery(theme.breakpoints.down('lg')));
});
return (
<Dialog open={dialogOpen}>
<DialogTitle>This is a test Dialog Title</DialogTitle>
</Dialog>
);
};
However this is giving me the error:
Uncaught Error: Invalid hook call. Hooks can only be called inside of
the body of a function component. This could happen for one of the
following reasons:
It seems it doesn't allow me to use hooks instide the useEffect hook.
How can I fix it otherwise, in order to achieve the same result, which is to update the useState state true or false according to the resolution of the browser?
You cannot use hooks inside a hook call (useMediaQuery in the callback of useEffect) even though you can use hooks inside another hook definition.
Fix it by moving useMediaQuery to the top level of MyDialog component.
Then use the output value in useEffect.
import { useTheme, useMediaQuery, Dialog, DialogTitle } from "#mui/material";
import { useEffect, useState } from "react";
const MyDialog = () => {
const theme = useTheme();
const [dialogOpen, setDialogOpen] = useState(false);
const matches = useMediaQuery(theme.breakpoints.down("lg"));
useEffect(() => {
setDialogOpen(matches);
});
return (
<Dialog open={dialogOpen}>
<DialogTitle>This is a test Dialog Title</DialogTitle>
</Dialog>
);
};
Hook should be called only on top level
You can only call Hooks while React is rendering a function component:
✅ Call them at the top level in the body of a function component. ✅
Call them at the top level in the body of a custom Hook.
const theme = useTheme();
const mediaQuery = useMediaQuery(theme.breakpoints.down('lg'))
Try to do setState only on mount to avoid infinite re-renders, Since you're computing browser specification setDialogOpen can be set once.
useEffect(() => {
setDialogOpen(mediaQuery);
},[]);
Read more about your error here
Related
I am implementing to change the state depending on it is hovered or not. But if I make it like the first code below, Too many re-renders. error will occur. In order to solve this problem, I used the useEffect to render only when the state changes, but an error called onHover is not defined occurs that the function declared in the user effect was not declared.
not using useEffect
import "./styles.css";
import { useState } from "react";
export default function App() {
const [isHover, setIsHover] = useState(false);
return (
<button onMouseEnter={setIsHover(true)} onMouseLeave={setIsHover(false)}>
click
</button>
);
}
using useEffect
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [isHover, setIsHover] = useState(false);
useEffect(()=>{
const onHover = () => {
setIsHover(true)
}
},[isHover])
return (
<button onMouseEnter={onHover()} onMouseLeave={setIsHover(false)}>
click
</button>
);
}
What should I do to use the function declared in useEffect?
As you just want to setState so no need to use useEffect.
You can use without using useEffect as below.
import "./styles.css";
import { useState } from "react";
export default function App() {
const [isHover, setIsHover] = useState(false);
return (
<button onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
click
</button>
);
}
It has to do with scope. The onHover function is defined within the useEffect hook, so it goes out of scope once you're out of the hook's block. You'll have to define it directly inside the component, outside of any other block scope, and simply call it inside useEffect.
It will still result in onHover called so many times until the mouse leaves the element in question. To prevent it, you could add a condition like so:
const onHover = () => {
if (!isHover) {
setIsHover(true);
}
}
I don't think I am using useEffect correctly in the function I have written. If I do the same with classes there is no problem and I am able to do ComponentDidMount(). But I don't know Hooks very well. So I think I am going wrong somewhere.
//import { onValue } from 'firebase/database';
import React, { useEffect } from "react";
import {db} from '../../firebase';
import {ref,onValue} from 'firebase/database'
import Grid from '#mui/material/Grid';
import Box from '#mui/material/Box';
export default function NiftyChart() {
React.useEffect(()=>{
const dbRef=ref(db,"1rh1Ta-8dqZKmh1xy5ans2lOqReoiVAT81WyDKqRaxl0/Nifty");
onValue(dbRef,(snapshot)=>{
let records=[];
snapshot.forEach(childSnapshot=>{
let keyName=childSnapshot.key;
let data=childSnapshot.val();
console.log(snapshot.val());
records.push({"key":keyName,"data":data})
console.log(records[records.length-1])
});
this.setState();
})
},[]);
return(
<div>
{this.state.map((row, index)=>{
return(
<Box component="span" sx={{}}>
<Grid > {row.Close}</Grid>
</Box>
)
})}
</div>
)
}
Also, no value is printed for row.Close. In the console I seem to be getting Cannot read properties of undefined (reading 'state') this error.
Any help is appreciated.
You need to use useState:
const [records, setRecords] = useState([]);
useEffect(() => ..get records and set to state, []);
So code would look like this:
export default function MyComponent() {
const [records, setRecords] = useState([]);
useEffect(() => {
// ... other code is omitted for the brevity
setRecords(records)
}, [])
return(
<div>
{records &&
records.map((row, index)=>{ //...other code is omitted
// for the brevity }
</div>
)
}
What's hook? The hook is:
A Hook is a special function that lets you “hook into” React features.
For example, useState is a Hook that lets you add React state to
function components
[] in useEffect means:
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
Read more about API call in this great article
Unless I'm mistaken, I believe I've found a bug in how rerenders are triggered (or in this case, aren't) by the #testing-library/react package. I've got a codesandbox which you can download and reproduce in seconds:
https://codesandbox.io/s/asynchronous-react-redux-toolkit-bug-8sleu4?file=/README.md
As a summary for here, I've just got a redux store and I toggle a boolean value from false to true after some async activity in an on-mount useEffect in a component:
import React, { useEffect } from "react";
import { useAppDispatch } from "../hooks/useAppDispatch";
import { setMyCoolBoolean } from "../redux/slices/exampleSlice";
import AnotherComponent from "./AnotherComponent";
export default function InnerComponent() {
const dispatch = useAppDispatch();
const fetchSomeData = async () => {
await fetch("https://swapi.dev/api/people");
dispatch(setMyCoolBoolean(true));
};
// on mount, set some values
useEffect(() => {
fetchSomeData();
}, []);
return <AnotherComponent />;
}
Then, in a different component, I hook into that store value with useAppSelector hook and then useEffect to do something local there (dumb example, but it illustrates my point.):
import { useEffect, useState } from "react";
import { useAppSelector } from "../hooks/useAppSelector";
export default function AnotherComponent() {
const { myCoolBoolean } = useAppSelector((state) => state.example);
const [localBoolean, setLocalBoolean] = useState(false);
// when myCoolBoolean changes, set the local boolean state value in this component
// somewhat a dumb example but it illustrates
// the failure of react-testing-library
useEffect(() => {
// only do something in this component
// if myCoolBoolean changes to true
if (myCoolBoolean) {
console.log("SET TO TRUE!");
setLocalBoolean(myCoolBoolean);
}
}, [myCoolBoolean]);
if (localBoolean) {
return <span data-testid="NEW">I'm new</span>;
}
return <span data-testid="ORIGINAL">I'm original</span>;
}
In result, my test would like to see if the 'new' value is ever shown. Despite issuing rerender, you will see the test fails:
import React from "react";
import { render } from "#testing-library/react";
import App from "../../src/App";
import { act } from "react-test-renderer";
import 'whatwg-fetch'
test("On mount, boolean value changes, causing our new span to show up", async () => {
const { getByTestId, rerender } = render(<App />);
await act(async () => {
// expect(getByTestId("ORIGINAL")).toBeTruthy();
// No matter how many times you call rerender here,
// you'll NEVER see the "NEW" test id (and thus corresponding <span> element) appear in the document
// despite this being the case in any standard browser
await rerender(<App />);
// If you comment this line below out, the test passes fine.
// test ID "ORIGINAL" is found, but "NEW" is never found!!!!
expect(getByTestId("NEW")).toBeTruthy();
});
});
Behaviour is totally as expected in a browser, but fails in my jest test. Can anybody guide me on how to get my test to pass? As far as I know, the code and implementations of my React components and Redux are the cleanest and best practices that are currently out there, so I'm more expecting this is a gross misunderstanding on my part of how #testing-library works, though I thought rerender would do the trick.
I've apparently misunderstood how react-testing-library works under the hood. You don't even need to use rerender or act at all! Simply using a waitFor with await / async is enough to trigger the on mount logic and subsequent rendering:
import React from "react";
import { findByTestId, render, waitFor } from "#testing-library/react";
import App from "../../src/App";
import { act } from "#testing-library/react-hooks/dom";
import "whatwg-fetch";
test("On mount, boolean value changes, causing our new span to show up", async () => {
const { getByTestId, rerender, findByTestId } = render(<App />);
// Works fine, as we would expect
expect(getByTestId("ORIGINAL")).toBeTruthy();
// simply by using 'await' here, react-testing-library must rerender somehow
// note that 'act' isn't even used or needed either!
await waitFor(() => getByTestId("NEW"));
});
Another case of "overthinking it" gone bad...
With react hooks coming, should we use prop-types for React custom hooks e.g,
import React from 'react';
import PropTypes from 'prop-types';
const useTitle = title => {
React.useEffect(() => {
document.title = title;
}, [title]);
}
useTitle.propTypes = {
title: PropTypes.string.isRequired,
};
export default useTitle;
Is the above a good approach to validate the param(s) passed to a custom react hooks or should there be a different way for validation the props/params passed to custom hook which is basically just a simple function.
No. React doesn't validate custom hooks or in-built hooks props.
Look here for updateFunctionComponent, It validates prop types using checkPropTypes for a react component and then render it with hooks i.e. check out renderWithHooks.
Now if you check here in renderWithHooks method, It updates the dispatcher and calls your functional component which in turn calls your custom hook, since it's just another function call inside your functional component.
Your custom hook will call in-built hooks. You can check the implementation here . Based on type of dispatcher it will call built-in hooks.
If you search checkPropTypes in the whole file you won't find the validation logic or prop-types/checkPropTypes dependency which is used to validate the prop types.
Here is some nice article about how react hooks works
I'm using PropTypes.checkPropTypes for useSelector hook. And it works for me.
const useTitle = title => {
React.useEffect(() => {
document.title = withPropsValidation(title);
}, [title]);
}
const withPropsValidation = props => {
PropTypes.checkPropTypes(propTypes, props, 'prop', '')
return props
}
const propTypes = {
title: PropTypes.string.isRequired,
}
https://github.com/facebook/prop-types#proptypescheckproptypes
In my opinion, using some kind of type mechanism would be better like TypeScript but if you don't you should still use propTypes and additionally you can check this course out from kentcdodds to be sure about propTypes usage https://egghead.io/courses/simplify-react-apps-with-react-hooks
The OP's approach has an issue in that the line const useTitle = title => { ... }; effectively makes title be the name of the parameter commonly named props. The useTitle.propTypes assignment, then, is not accurate.
The OP's approach, however, is viable (with modification). Below is an implementation showing how to use PropTypes with a custom hook:
MyHook.jsx:
import { useEffect } from 'react';
import PropTypes from 'prop-types';
const useMyHook= ({ callback, ref }) => {
...do something...
};
useMyHook.propTypes = {
callback: PropTypes.func.isRequired,
ref: PropTypes.element.isRequired
};
export { useMyHook };
MyComponent.jsx:
import React, { useRef } from 'react';
import { useMyHook} from '.../MyHook.jsx';
const MyComponent = props => {
const myRef = useRef(null);
const myCallback = () => console.log('Hello');
useMyHook({ ref: myRef, callback, myCallback });
return <div ref={myRef}>Stuff...</div>;
};
Typescript is the best way for validation and check props
import React from 'react';
const useTitle = ({title}:{title?:string})=> {
React.useEffect(() => {
document.title = title;
}, [title]);
}
export default useTitle;
Or
import React from 'react';
type customPropType= {
title?:string
}
const useTitle = ({title}:customPropType)=> {
React.useEffect(() => {
document.title = title;
}, [title]);
}
export default useTitle;
Is there a way with new react hooks API to replace a context data fetch?
If you need to load user profile and use it almost everywhere, first you create context and export it:
export const ProfileContext = React.createContext()
Then you import in top component, load data and use provider, like this:
import { ProfileContext } from 'src/shared/ProfileContext'
<ProfileContext.Provider
value={{ profile: profile, reloadProfile: reloadProfile }}
>
<Site />
</ProfileContext.Provider>
Then in some other components you import profile data like this:
import { ProfileContext } from 'src/shared/ProfileContext'
const context = useContext(profile);
But there is a way to export some function with hooks that will have state and share profile with any component that want to get data?
React provides a useContext hook to make use of Context, which has a signature like
const context = useContext(Context);
useContext accepts a context object (the value returned from
React.createContext) and returns the current context value, as given
by the nearest context provider for the given context.
When the provider updates, this Hook will trigger a rerender with the
latest context value.
You can make use of it in your component like
import { ProfileContext } from 'src/shared/ProfileContext'
const Site = () => {
const context = useContext(ProfileContext);
// make use of context values here
}
However if you want to make use of the same context in every component and don't want to import the ProfileContext everywhere you could simply write a custom hook like
import { ProfileContext } from 'src/shared/ProfileContext'
const useProfileContext = () => {
const context = useContext(ProfileContext);
return context;
}
and use it in the components like
const Site = () => {
const context = useProfileContext();
}
However as far a creating a hook which shares data among different component is concerned, Hooks have an instance of the data for them self and don'tshare it unless you make use of Context;
updated:
My previous answer was - You can use custom-hooks with useState for that purpose, but it was wrong because of this fact:
Do two components using the same Hook share state? No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
The right answer how to do it with useContext() provided #ShubhamKhatri
Now i use it like this.
Contexts.js - all context export from one place
export { ClickEventContextProvider,ClickEventContext} from '../contexts/ClickEventContext'
export { PopupContextProvider, PopupContext } from '../contexts/PopupContext'
export { ThemeContextProvider, ThemeContext } from '../contexts/ThemeContext'
export { ProfileContextProvider, ProfileContext } from '../contexts/ProfileContext'
export { WindowSizeContextProvider, WindowSizeContext } from '../contexts/WindowSizeContext'
ClickEventContext.js - one of context examples:
import React, { useState, useEffect } from 'react'
export const ClickEventContext = React.createContext(null)
export const ClickEventContextProvider = props => {
const [clickEvent, clickEventSet] = useState(false)
const handleClick = e => clickEventSet(e)
useEffect(() => {
window.addEventListener('click', handleClick)
return () => {
window.removeEventListener('click', handleClick)
}
}, [])
return (
<ClickEventContext.Provider value={{ clickEvent }}>
{props.children}
</ClickEventContext.Provider>
)
}
import and use:
import React, { useContext, useEffect } from 'react'
import { ClickEventContext } from 'shared/Contexts'
export function Modal({ show, children }) {
const { clickEvent } = useContext(ClickEventContext)
useEffect(() => {
console.log(clickEvent.target)
}, [clickEvent])
return <DivModal show={show}>{children}</DivModal>
}