mock value inside the component file - javascript

I would like to ask if I have the variable useState in the component which is used as a condition that determines the element inside it will appear or not. How to mock the variable? So that I can test the element inside the condition if the value is 'login'.
const [dataHistory,seDataHistory] = useState("")
const [data,setData] = useState("firstTimeLogin")
const func2 = () => {
getData({
item1: "0",
}).
then((res) => {
funct();
})
}
const funct = () =>
{setData("login")}
return
{data === "firstTimeLogin" && (
<div><button onClick="funct2()">next</button></div>
)}
{data === "login" && (
<div>flow login</div>
)}

Firstly, you need to add data-testid for your button
{data === "firstTimeLogin" && (
<div><button onClick="funct2" data-testid="next-button">next</button></div>
)}
You called onClick="funct2()" which is to trigger funct2 immediately after re-rendering, so I modified it to onClick="funct2".
Note that you also can use next content in the button for the event click but I'd prefer using data-testid is more fixed than next content.
In your test file, you should mock getData and call fireEvent.click to trigger funct2()
import { screen, fireEvent, render } from '#testing-library/react';
//'/getData' must be the actual path you're importing for `getData` usage
jest.mock('/getData', () => {
return {
getData: () => new Promise((resolve) => { resolve('') }) // it's mocked, so you can pass any data here
}
})
it('should call funct', async () => {
render(<YourComponent {...yourProps}/>)
const nextButton = await screen.findByTestId("next-button"); //matched with `data-testid` we defined earlier
fireEvent.click(nextButton); //trigger the event click on that button
const flowLoginElements = await screen.findByText('flow login'); //after state changes, you should have `flow login` in your content
expect(flowLoginElements).toBeTruthy();
})

You can create a button and onClick of the button call funct()

Related

Fetch not working on first click of onClick function

When I use modalOpen in a onClick function it wont fetch api on the 1st click causing the code to break but it will on 2nd click what can cause it
// get anime result and push to modal function
const modalAnime = async () => {
const { data } = await fetch(`${base_url}/anime/${animeId}`)
.then((data) => data.json())
.catch((err) => console.log(err));
SetAnimeModalData(data);
};
I am trying to get the fetch to work on the first click but it doesn't until second or third click
const modalOpen = (event) => {
SetAnimeId(event.currentTarget.id);
SetModalVisible(true);
modalAnime();
console.log(animeId);
};
const modalClose = () => {
SetModalVisible(false);
SetAnimeId("");
};
return (
<div className="app">
<Header
searchAnime={searchAnime}
search={search}
SetSearch={SetSearch}
mostPopular={mostPopular}
topRated={topRated}
/>
{loadingState ? (
<ResultLoading />
) : (
<Results
animeResults={animeResults}
topRated={topRated}
mostPopular={mostPopular}
modalOpen={modalOpen}
/>
)}
{modalVisible ? <AnimeInfoModal modalClose={modalClose} /> : <></>}
</div>
);
the modal opens fine but the ID isn't captured until the second or third click
I have more code but Stack Overflow won't let me add it.
SetAnimeId() won't update the animeId state property until the next render cycle. Seems like you should only update the visible and animeId states after you've fetched data.
You should also check for request failures by checking the Response.ok property.
// this could be defined outside your component
const fetchAnime = async (animeId) => {
const res = await fetch(`${base_url}/anime/${animeId}`);
if (!res.ok) {
throw res;
}
return (await res.json()).data;
};
const modalOpen = async ({ currentTarget: { id } }) => {
// Await data fetching then set all new state values
SetAnimeModalData(await fetchAnime(id));
SetAnimeId(id);
SetModalVisible(true);
};

Fixing hook call outside of the body of a function component

I made a custom ReactJS hook to handle a couple of specific mouse events, as below:
const HealthcareServices = ({
filterToRemove,
filters,
onChange,
onClear,
selectedAmbulatoryCareFilterValue,
shouldClear,
}: Props): JSX.Element => {
const classes = useStyles();
...
useEffect(() => {
shouldClear && clearFilters();
}, [shouldClear]);
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
const handleSelectedItem = (service: Filter) => {
service.selected = !service.selected;
setHealthcareServices([...healthcareServices]);
onChange(healthcareServices);
};
const handleSingleClick = (service: Filter) => {
console.log('single-click');
if (service.isRequired) {
service.checkedIcon = <Icons.CheckboxSingleClick />;
}
handleSelectedItem(service);
};
const handleDoubleClick = (service: Filter) => {
console.log('double-click');
if (service.isRequired) {
service.checkedIcon = <Icons.CheckboxDoubleClick />;
}
handleSelectedItem(service);
};
const handleClick = (service: Filter) =>
useSingleAndDoubleClick(
() => handleSingleClick(service),
() => handleDoubleClick(service)
);
...
return (
<div className={classes.filter_container}>
...
<div className={classes.filter_subgroup}>
{filters.map((filter) => (
<div key={`${filter.label}-${filter.value}`} className={classes.filter}>
<Checkbox
label={filter.label}
className={classes.checkbox}
checked={filter.selected}
onChange={() => handleClick(filter)}
checkedIcon={filter.checkedIcon}
/>
</div>
))}
</div>
...
</div>
);
};
When I click on my <Checkbox />, the whole thing crashes. The error is:
The top of my stacktrace points to useState inside my hook. If I move it outside, so the hook looks as:
const [click, setClick] = useState(0);
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
The problem still happens, only the stacktrace points to the useEffect hook. The code is based on another answer here.
Any suggestions?
You've defined your useSingleAndDoubleClick hook inside of a component. That's not what you want to do. The idea of custom hooks is that you can move logic outside of your components that could otherwise only happen inside of them. This helps with code reuse.
There is no use for a hook being defined inside a function, as the magic of hooks is that they give you access to state variables and such things that are usually only allowed to be interacted with inside function components.
You either need to define your hook outside the component and call it inside the component, or remove the definition of useSingleAndDoubleClick and just do everything inside the component.
EDIT: One more note to help clarify: the rule that you've really broken here is that you've called other hooks (ie, useState, useEffect) inside your useSingleAndDoubleClick function. Even though it's called useSingleAndDoubleClick, it's not actually a hook, because it's not being created or called like a hook. Therefore, you are not allowed to call other hooks inside of it.
EDIT: I mentioned this earlier, but here's an example that could work of moving the hook definition outside the function:
EDIT: Also had to change where you call the hook: you can't call the hook in a nested function, but I don't think you need to.
const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
const [click, setClick] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
// simple click
if (click === 1) actionSimpleClick();
setClick(0);
}, delay);
// the duration between this click and the previous one
// is less than the value of delay = double-click
if (click === 2) actionDoubleClick();
return () => clearTimeout(timer);
}, [click]);
return () => setClick((prev) => prev + 1);
};
const HealthcareServices = ({
filterToRemove,
filters,
onChange,
onClear,
selectedAmbulatoryCareFilterValue,
shouldClear,
}: Props): JSX.Element => {
const classes = useStyles();
...
useEffect(() => {
shouldClear && clearFilters();
}, [shouldClear]);
// your other handlers
// changed this - don't call the hook inside the function.
// your hook is returning the handler you want anyways, I think
const handleClick = useSingleAndDoubleClick(handleSingleClick, handleDoubleClick)

How to trigger a function automatically when the data in an array changes?

i just started React js and im trying to create a simple recipe web with API.
I am trying to create a page that will display the data of favorite recipes from an array in local storage using map() like below.
const FavRecipes = () => {
const [recipeArray, setRecipeArray] = useState([]);
const refreshData = () => {
const existedFavRecipe = localStorage.getItem("FavRecipes");
const data = existedFavRecipe !== null ? JSON.parse(existedFavRecipe) : [];
setRecipeArray(data);
}
return (
<FavRecipesContainer>
{recipeArray.map( e => (
<>
<FavRecipeImage src ={e.image} />
<FavRecipeTitle>{e.title}</FavRecipeTitle>
</>
))}
</FavRecipesContainer>
)
}
The problem is I want the function of refreshData to get triggered automatically everytime the data in the array changes because i will create a delete button that can delete the favorite recipes. I am thinking of using useEffect() but I dont know how to do it. Is there any suggestion to solve this? Would appreciate it!
I want the function of refreshData to get triggered automatically everytime the data in the array changes because i will create a delete button that can delete the favorite recipes..
The problem is that there is no event that fires when local storage is changed by other code in the same window (the storage event only fires when storage is changed in other windows).
There are dodgy solutions like these, but really just make sure that your deletion code calls refreshData as part of its logic. Or actually, you don't even need refreshData, you could maintain the array locally in the component and just echo it to local storage:
const FavRecipes = () => {
const [recipeArray, setRecipeArray] = useState([]);
// Initial data load
useEffect(() => {
const existedFavRecipe = localStorage.getItem("FavRecipes");
const data = existedFavRecipe !== null ? JSON.parse(existedFavRecipe) : [];
setRecipeArray(data);
}, []);
// Deletion
const deleteRecipe = (recipe) => {
setRecipeArray(recipes => {
recipes = recipes.filter(r => r !== recipe);
localStorage.setItem("FavRecipes", JSON.stringify(recipes));
return recipes;
});
};
return (
<FavRecipesContainer>
{recipeArray.map( recipe => (
<>
<FavRecipeImage src ={recipe.image} />
<FavRecipeTitle>{recipe.title}</FavRecipeTitle>
<button onClick={() => deleteRecipe(recipe)}>X</button>
</>
))}
</FavRecipesContainer>
);
};
If you also want to listen for changes in other windows (users do that):
const FavRecipesKey = "FavRecipes";
const FavRecipes = () => {
const [recipeArray, setRecipeArray] = useState([]);
// Initial data load and watch for changes in other windows
useEffect(() => {
function refreshData() {
const existedFavRecipe = localStorage.getItem(FavRecipesKey);
const data = existedFavRecipe !== null ? JSON.parse(existedFavRecipe) : [];
setRecipeArray(data);
}
function storageEventHandler({key}) {
if (key === FavRecipesKey) {
refreshData();
}
}
refreshData();
window.addEventListener("storage", storageEventHandler);
return () => {
window.removeEventListener("storage", storageEventHandler);
};
}, []);
// Deletion
const deleteRecipe = (recipe) => {
setRecipeArray(recipes => {
recipes = recipes.filter(r => r !== recipe);
localStorage.setItem(FavRecipesKey, JSON.stringify(recipes));
return recipes;
});
};
return (
<FavRecipesContainer>
{recipeArray.map( recipe => (
<>
<FavRecipeImage src ={recipe.image} />
<FavRecipeTitle>{recipe.title}</FavRecipeTitle>
<button onClick={() => deleteRecipe(recipe)}>X</button>
</>
))}
</FavRecipesContainer>
);
};
This is a tough one. The closest one in React camp is useMutableSource. https://github.com/reactjs/rfcs/blob/main/text/0147-use-mutable-source.md
However useMutableSource is a bit too advanced. So maybe we should think of the problem in another way. For instance, if you can know the time or component who invokes localStorage.setItem, then you can turn it in a context.
Define a context
Create a file that you can share to other components.
const RecipeContext = React.createContext()
export default RecipeContext
Import it to set it
When you want to set the content, import the context and write it via current.
import RecipeContext from './RecipeContext'
const AComponent = () => {
const recipe = React.useContext(RecipeContext)
recipe.current = recipeArray
}
Import it to read it
When you want to read out the current value, import the context and read it via current.
import RecipeContext from './RecipeContext'
const BComponent = () => {
const recipe = React.useContext(RecipeContext)
const onClick = () => {
console.log(recipe.current)
}
}
You should be able to use RecipeContext as a "global" variable similar to localStorage. Even better if you have any default value, you can set it at the time you create it.
const RecipeContext = React.createContext(defaultRecipeArray)
You don't even need a provider <RecipeContext.Provider />, because you are using it as a "global" context, a very special usage.

Component not updating after fireEvent.click

I'm new to Jest and the testing library. I have a NavbarContainer component that renders one button or another depending on a variable (menuOpen) that it's being changed with a function. I have already checked that the variable changes its value after the fireEvent, however, it seems like the component it's not updating. What am I doing wrong?
Here there is my component
export const NavbarContainer = ({functionality, menuOpen, activeLink}) => {
return (
<div className="navbar-container">
{ menuOpen && <Navbar functionality={functionality} activeLink={activeLink}/> }
{ menuOpen && <Button text="navbar.back" functionality={functionality}></Button> }
{ !menuOpen && <Button text="navbar.menu" functionality={functionality} modificator="back"></Button> }
</div>
);
};
Here is the button
export const Button = ({ text, type = "button", functionality, disabled = false}) => {
return (
<button onClick={functionality} type={type} disabled={disabled}>{i18n.t(text)}</button>
);
};
Here are the values and functions I am passing to the NavbarContainer component
const [menuOpen, setMenuOpen] = useState(false);
const [activeLink, setActiveLink] = useState("");
const openMenu = (link) => {
setMenuOpen(!menuOpen);
setActiveLink(link.toString());
};
Here are my tests
describe("NavbarContainer", () => {
i18n.changeLanguage('cimode');
let component;
let menuOpen = true;
const openMenu = () => {
menuOpen = !menuOpen;
};
beforeEach(() => {
component = render(
<BrowserRouter basename="/">
<NavbarContainer menuOpen={menuOpen} functionality={openMenu} activeLink=""/>
</BrowserRouter>
);
});
it("after click the menu button is shown", async () => {
const backButton = component.queryByText("navbar.back");
await fireEvent.click(backButton);
const menuButton = await component.queryByText("navbar.menu");
expect(menuButton).toBeInTheDocument();
});
});
Here is the error I'm getting
expect(received).toBeInTheDocument()
the received value must be an HTMLElement or an SVG element.
Received has value: null
Since it might take a few seconds for your navbar.menu to appear you after your click, you need to use findBy to try and select your item. This adds a timeout of 5 seconds to try and find the item. If it still hasn't appeared after 5 seconds, then there is probably something else going on with your code.
If you want your test to be async, then I would recommend that your first call also be a findBy rather than a queryBy since queryBy isn't an asynchronous call.
it("after click the menu button is shown", async () => {
const backButton = await component.findyByText("navbar.back");
await fireEvent.click(backButton);
const menuButton = await component.findByText("navbar.menu");
expect(menuButton).toBeInTheDocument();
});
You also probably don't need that last expect call because if it can't find the navbar.menu item via the previous call, then it would fail anyway. Same if it finds it.

React Hooks multiple alerts with individual countdowns

I've been trying to build an React app with multiple alerts that disappear after a set amount of time. Sample: https://codesandbox.io/s/multiple-alert-countdown-294lc
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function TimeoutAlert({ id, message, deleteAlert }) {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
}
let _ID = 0;
function App() {
const [alerts, setAlerts] = useState([]);
const addAlert = message => setAlerts([...alerts, { id: _ID++, message }]);
const deleteAlert = id => setAlerts(alerts.filter(m => m.id !== id));
console.log({ alerts });
return (
<div className="App">
<button onClick={() => addAlert("test ")}>Add Alertz</button>
<br />
{alerts.map(m => (
<TimeoutAlert key={m.id} {...m} deleteAlert={deleteAlert} />
))}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The problem is if I create multiple alerts, it disappears in the incorrect order. For example, test 0, test 1, test 2 should disappear starting with test 0, test 1, etc but instead test 1 disappears first and test 0 disappears last.
I keep seeing references to useRefs but my implementations don't resolve this bug.
With #ehab's input, I believe I was able to head down the right direction. I received further warnings in my code about adding dependencies but the additional dependencies would cause my code to act buggy. Eventually I figured out how to use refs. I converted it into a custom hook.
function useTimeout(callback, ms) {
const savedCallBack = useRef();
// Remember the latest callback
useEffect(() => {
savedCallBack.current = callback;
}, [callback]);
// Set up timeout
useEffect(() => {
if (ms !== 0) {
const timer = setTimeout(savedCallBack.current, ms);
return () => clearTimeout(timer);
}
}, [ms]);
}
You have two things wrong with your code,
1) the way you use effect means that this function will get called each time the component is rendered, however obviously depending on your use case, you want this function to be called once, so change it to
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
}, []);
adding the empty array as a second parameter, means that your effect does not depend on any parameter, and so it should only be called once.
Your delete alert depends on the value that was captured when the function was created, this is problematic since at that time, you don't have all the alerts in the array, change it to
const deleteAlert = id => setAlerts(alerts => alerts.filter(m => m.id !== id));
here is your sample working after i forked it
https://codesandbox.io/s/multiple-alert-countdown-02c2h
well your problem is you remount on every re-render, so basically u reset your timers for all components at time of rendering.
just to make it clear try adding {Date.now()} inside your Alert components
<button onClick={onClick}>
{message} {id} {Date.now()}
</button>
you will notice the reset everytime
so to achieve this in functional components you need to use React.memo
example to make your code work i would do:
const TimeoutAlert = React.memo( ({ id, message, deleteAlert }) => {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
},(oldProps, newProps)=>oldProps.id === newProps.id) // memoization condition
2nd fix your useEffect to not run cleanup function on every render
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
finally something that is about taste, but really do you need to destruct the {...m} object ? i would pass it as a proper prop to avoid creating new object every time !
Both answers kind of miss a few points with the question, so after a little while of frustration figuring this out, this is the approach I came to:
Have a hook that manages an array of "alerts"
Each "Alert" component manages its own destruction
However, because the functions change with every render, timers will get reset each prop change, which is undesirable to say the least.
It also adds another lay of complexity if you're trying to respect eslint exhaustive deps rule, which you should because otherwise you'll have issues with state responsiveness. Other piece of advice, if you are going down the route of using "useCallback", you are looking in the wrong place.
In my case I'm using "Overlays" that time out, but you can imagine them as alerts etc.
Typescript:
// useOverlayManager.tsx
export default () => {
const [overlays, setOverlays] = useState<IOverlay[]>([]);
const addOverlay = (overlay: IOverlay) => setOverlays([...overlays, overlay]);
const deleteOverlay = (id: number) =>
setOverlays(overlays.filter((m) => m.id !== id));
return { overlays, addOverlay, deleteOverlay };
};
// OverlayIItem.tsx
interface IOverlayItem {
overlay: IOverlay;
deleteOverlay(id: number): void;
}
export default (props: IOverlayItem) => {
const { deleteOverlay, overlay } = props;
const { id } = overlay;
const [alive, setAlive] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setAlive(false), 2000);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => {
if (!alive) {
deleteOverlay(id);
}
}, [alive, deleteOverlay, id]);
return <Text>{id}</Text>;
};
Then where the components are rendered:
const { addOverlay, deleteOverlay, overlays } = useOverlayManger();
const [overlayInd, setOverlayInd] = useState(0);
const addOverlayTest = () => {
addOverlay({ id: overlayInd});
setOverlayInd(overlayInd + 1);
};
return {overlays.map((overlay) => (
<OverlayItem
deleteOverlay={deleteOverlay}
overlay={overlay}
key={overlay.id}
/>
))};
Basically: Each "overlay" has a unique ID. Each "overlay" component manages its own destruction, the overlay communicates back to the overlayManger via prop function, and then eslint exhaustive-deps is kept happy by setting an "alive" state property in the overlay component that, when changed to false, will call for its own destruction.

Categories

Resources