How to test prop function that changes other prop Jest Enzyme - javascript

I have a component that receives value 'openDrawer' (bool) and function 'toggleDrawerHandler' in props, the function 'toggleDrawerHandler' changes the value of 'openDrawer' prop.
I would like to test it by simulating a click on div that triggers this function, and check if the component change when the value of 'openDrawer' changes.
The component
const NavigationMobile = (props) => {
const { openDrawer, toggleDrawerHandler } = props;
let navClass = ["Nav-Mobile"];
if (!openDrawer) navClass.push("Close");
return (
<div className="Mobile">
<div className="Menubar" onClick={toggleDrawerHandler}>
{openDrawer ? <FaTimes size="1.5rem" /> : <FaBars size="1.5rem" />}
</div>
<nav className={navClass.join(" ")} onClick={toggleDrawerHandler}>
<Navigation />
</nav>
</div>
);
};
The component that sends these props
const Header = (props) => {
const [openDrawer, setOpenDrawer] = useState(false);
const toggleDrawerHandler = () => {
setOpenDrawer((prevState) => !prevState);
};
return (
<header className="Header">
<NavigationMobile openDrawer={openDrawer} toggleDrawerHandler={toggleDrawerHandler} />
</header>
);
};
my test, but doesn't work
it("changes prop openDrawer when click", () => {
const wrapper = shallow(<NavigationMobile />);
expect(wrapper.find("FaBars")).toHaveLength(1);
expect(wrapper.find("nav").hasClass("Nav-Mobile")).toBeTruthy();
wrapper.find(".Menubar").simulate("click", true); // doesnt work
expect(wrapper.find("FaTimes")).toHaveLength(1);
expect(wrapper.find("nav").hasClass("Nav-Mobile Close")).toBeTruthy();
});

Related

Too many React components re-rendering, how to limit it?

I am generating a list of components on the screen like so:
const MessagesContainer = ({ messages, categories, addHandler }) => {
const options = categories.map(category => (
{ value: category.name, label: category.name }
));
return (
<div className="d-flex flex-wrap justify-content-center">
{messages.map(message =>
<div key={message.id}>
<MessageEditor
message={message}
options={options}
addHandler={addHandler}
/>
</div>
)}
</div>
);
};
const MessageEditor = ({ message, options, addHandler }) => {
const [modifedMessage, setModifiedMessage] = useState(message);
const [isAdded, setIsAdded] = useState(false);
const textClass = (charLimit - modifedMessage.text.length) > 0 ?
'text-success' : 'text-danger';
const buttonClass = isAdded ? 'danger' : 'primary';
const ref = useRef(null);
const textAreaHandler = textArea => {
const copiedMessage = { ...modifedMessage };
copiedMessage.text = textArea.target.value;
setModifiedMessage(copiedMessage);
};
const addButtonHandler = () => {
const add = !isAdded;
setIsAdded(add);
let selectedCategoires = ref.current.state.value;
// Firing this handler results in ALL the MessageEditor
// componets on the screen being re-rendered
addHandler(modifedMessage, add, selectedCategoires);
}
return (
<div className="d-flex flex-column message-view-container ml-5 mr-5 mb-5">
<div className={`message-count-container ${textClass}`}>
{charLimit - modifedMessage.text.length}
</div>
<Select
ref={ref}
placeholder="Tags"
isMulti
name="tags"
options={options}
defaultValue={[options[0]]}
className="basic-multi-select select-container"
classNamePrefix="select"
isDisabled={isAdded}
/>
<Form.Control
style={{
width:350,
height:220,
resize:'none'
}}
className="mb-1"
as="textarea"
defaultValue={message.text}
onChange={textAreaHandler}
disabled={isAdded}
/>
<Button variant={buttonClass} onClick={addButtonHandler}>
{isAdded ? 'Remove' : 'Add'}
</Button>
</div>
);
};
And the parent component that holds the addHandler:
const { useState } = require("react");
const Messages = () => {
const [messages, setMessages] = useState([]);
const [saveMessages, setSaveMessages] = useState({});
const addHandler = (modifiedMessage, add, selectedCategoires) => {
const copiedSaveMessages = { ...saveMessages };
if (add) {
if (selectedCategoires) {
selectedCategoires = selectedCategoires.map(item => item.value);
}
copiedSaveMessages[modifiedMessage.id] = {
text: modifiedMessage.text,
tags: selectedCategoires ? selectedCategoires : []
}
} else {
delete copiedSaveMessages[modifiedMessage.id];
}
// This results in every single MessageEditor component being
// re-rendered
setSaveMessages(copiedSaveMessages);
};
return (
<div>
{categories &&
<div>
<div className="ml-5 mr-5 mt-5">
<MessagesContainer
messages={messages}
categories={categories}
addHandler={addHandler}
/>
</div>
</div>
}
{Object.keys(saveMessages).length > 0 &&
<div>
<Image
className="upload-icon"
src={uploadIcon}
/>
<div className="text-primary count-container">
<h2>{Object.keys(saveMessages).length}</h2>
</div>
</div>
}
</div>
);
};
The issue is that if I hit the add button an trigger addHandler it causes all the MessageEditor components to re-render. And the performance is very slow if I have a few hundred components on the screen.
I guess this is because the saveMessages state variable belongs to the Messages component and MessageEditor is a child of Messages so it also re-renders.
Is there an approach I can take to update this state without causing all the other components to re-render?
In Messages you should wrap your addHandler in a useCallback hook (React useCallback hook) so that it is not re-created at each render.
const addHandler = useCallback((modifiedMessage, add, selectedCategoires) => {
// function body...
}, []);
Additionally, you can also memoize MessageEditor using React.memo() (React memo).
const MessageEditor = React.memo(({ message, options, addHandler }) => {
// component body...
});

How to pass HTML attributes to child component in React?

I have a parent and a child component, child component has a button, which I'd like to disable it after the first click. This answer works for me in child component. However the function executed on click now exists in parent component, how could I pass the attribute down to the child component? I tried the following and it didn't work.
Parent:
const Home = () => {
let btnRef = useRef();
const handleBtnClick = () => {
if (btnRef.current) {
btnRef.current.setAttribute("disabled", "disabled");
}
}
return (
<>
<Card btnRef={btnRef} handleBtnClick={handleBtnClick} />
</>
)
}
Child:
const Card = ({btnRef, handleBtnClick}) => {
return (
<div>
<button ref={btnRef} onClick={handleBtnClick}>Click me</button>
</div>
)
}
In general, refs should be used only as a last resort in React. React is declarative by nature, so instead of the parent "making" the child disabled (which is what you are doing with the ref) it should just "say" that the child should be disabled (example below):
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = ({isDisabled, onButtonClick}) => {
return (
<div>
<button disabled={isDisabled} onClick={onButtonClick}>Click me</button>
</div>
)
}
Actually it works if you fix the typo in prop of Card component. Just rename hadnlBtnClick to handleBtnClick
You don't need to mention each prop/attribute by name as you can use javascript Object Destructuring here.
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = (props) => {
return (
<div>
<button {...props}>Click me</button>
</div>
)
}
You can also select a few props and use them differently in the child components. for example, see the text prop below.
const Home = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false)
const handleButtonClick = () => {
setIsButtonDisabled(true)
}
return (
<>
<Card text="I'm a Card" isDisabled={isButtonDisabled} onButtonClick={handleButtonClick} />
</>
)
}
const Card = ({text, ...restProps}) => {
return (
<div>
<button {...restProps}>{text}</button>
</div>
)
}

Is it possible to add ref to the props.children elements?

I have a Form and Input components, which are rendered as below.
<Form>
<Field />
<Field />
<Field />
</Form>
Form component will act as wrapper component here and Field component ref are not being set here. I want iterate through props.children in Form Component and want to assign a ref attribute to each children. Is there any possibility to achieve this?
You need Form to inject your refs with React.Children and React.cloneElement APIs:
const FunctionComponentForward = React.forwardRef((props, ref) => (
<div ref={ref}>Function Component Forward</div>
));
const Form = ({ children }) => {
const childrenRef = useRef([]);
useEffect(() => {
console.log("Form Children", childrenRef.current);
}, []);
return (
<>
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
ref: (ref) => (childrenRef.current[index] = ref)
})
)}
</>
);
};
const App = () => {
return (
<Form>
<div>Hello</div>
<FunctionComponentForward />
</Form>
);
};
You can map children create new instance of component based on it using one of two ways showed in React Docs.
With React.Children.map and React.cloneElement (this way, key and ref from original element are preserved)
Or only with React.Children.map (Only ref from original component is preserved)
function useRefs() {
const refs = useRef({});
const register = useCallback((refName) => ref => {
refs.current[refName] = ref;
}, []);
return [refs, register];
}
function WithoutCloneComponent({children, ...props}) {
const [refs, register] = useRefs();
return (
<Parent>
{React.Children.map((Child, index) => (
<Child.type
{...Child.props}
ref={register(`${field-${index}}`)}
/>
)}
</Parent>
)
}
function WithCloneComponent({children, ...props}) {
const [refs, register] = useRefs();
return (
<Parent>
{
React.Children.map((child, index) => React.cloneElement(
child,
{ ...child.props, ref: register(`field-${index}`) }
)
}
</Parent>
)
}

Add to favourites and view from favourites with React Hooks?

I have a state
const [ideas, setIdeas] = useState([{title:"test", favourite:false]);
Component Idea.jsx returns props.title and a button "fav".
App.jsx maps through the idea[] and renders each idea.title in
<Item title = {idea.title}/>
on the page.
Problem:
Every time when "fav" is clicked I want to toggle ideas[index].favourite.
How to change a value of favourite only for an idea that was clicked?
How to add this exact idea to the array favourites[]?
App.jsx
function App() {
const [ideas, setIdeas] = useState([{title:"test",
favourite:false}]);
const [isClicked, setIsClicked] = useState(false)
function showAllIdeas () {
setIsClicked(prevValue => {
return !prevValue
}
)
}
function mapIdeas(){return ideas.map((ideaItem, index) => {
return (<Idea
key = {index}
id = {index}
title = {ideaItem.title}
/>
);
})}
return ( <div>
<Fab color="primary" onClick={showAllIdeas}>{expandText()}</Fab>
{isClicked && mapIdeas()}
</div>)
}
Item.jsx
function Idea(props) {
const [isClicked, setIsClicked] = useState(false)
function handleClick(){
setIsClicked(prevValue => {
return !prevValue
})
}
console.log(isClicked)
return(
<div className={"idea-list" } ><p>{props.title} {isClicked ?
<StarIcon onClick={handleClick}/> :<StarBorderIcon onClick=.
{handleClick}/>}</p>
</div>
)
}
const handleFavToggle = (index) => {
setItems(items=> {
const data = [...items]
data[index] = {...data[index],favourite: !data[index].favourite }
return data
})
}
<Item key={index} title={item.title} index={index} handleFavToggle={handleFavToggle}/>
In item component you have to handle click with handleFavToggle and pass all params

How to pass ref to children of children using array of refs React?

I'm trying to pass multiple times refs to children of children but it is not working. I have a functional component called AvatarBuilder that uses the Avatars component. This component renders a list of Avatar components. The idea is to have in AvatarBuilder references to each of the Avatar component.
Here is the code snippet summarized:
const AvatarBuilder = props => {
...
// in this dummy example i would have 5 avatars
const URLS=[1,2,3,4,5];
const refs = URLS.map(item => ({ ref: React.createRef() }));
return (
<>
<Avatars
ref={refs}
urlList={URLS}
/>
</>
);
const Avatars = React.forwardRef((props, ref) => {
let urlList = props.urlList.map((url, index) => {
return (
<Avatar
ref={ref[index].ref}
url={url}
/>
)
})
return (
<ul className="Avatars" >
{urlList}
</ul>
)
});
const Avatar = React.forwardRef((props, ref) => {
return (
<li className="Avatar">
<img
src={props.url}
ref={ref}
/>
</li>
)
});
I get the following warning and the refs array is not updated when all the components are mounted.
index.js:1 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Check the render method of `ForwardRef`.
in h (at Avatar.js:11)
in li (at Avatar.js:10)
in ForwardRef (at Avatars.js:10)
in ul (at Avatars.js:24)
in ForwardRef (at AvatarBuilder.js:189)
in AvatarBuilder (at App.js:19)
in div (at App.js:14)
in App (at src/index.js:9)
in StrictMode (at src/index.js:8)
Any idea how should this be fixed? Thanks!
For a functional component, you must use useRef and not React.createRef since a new instance of refs will be created on in render.
If you use React.createRef, then make use of useMemo to memoize the refs
const AvatarBuilder = props => {
// in this dummy example i would have 5 avatars
const URLS=[1,2,3,4,5];
const refs = React.useMemo(() =>URLS.map(item => ({ ref: React.createRef() })), []); // create refs only once
React.useEffect(() => {
console.log(refs);
},[])
return (
<Avatars
ref={refs}
urlList={URLS}
/>
);
}
const Avatars = React.forwardRef((props, ref) => {
let urlList = props.urlList.map((url, index) => {
return (
<Avatar
ref={ref[index].ref}
url={url}
/>
)
})
return (
<ul className="Avatars" >
{urlList}
</ul>
)
});
const Avatar = React.forwardRef((props, ref) => {
return (
<li className="Avatar">
<img
src={props.url}
ref={ref}
/>
</li>
)
});
ReactDOM.render(<AvatarBuilder/>, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app"/>
Try this one.
const AvatarBuilder = props => {
...
// in this dummy example i would have 5 avatars
const URLS=[1,2,3,4,5];
const refs = URLS.map(item => React.createRef());
return (
<>
<Avatars
refs={refs}
urlList={URLS}
/>
</>
);
// you don't need to make it with `fowardRef()`
const Avatars = (props) => {
const {refs} = props;
let urlList = props.urlList.map((url, index) => {
console.log(url, index, typeof (index), ref);
return (
<Avatar
ref={refs[index]}
url={url}
/>
)
})
return (
<ul className="Avatars" >
{urlList}
</ul>
)
};
const Avatar = React.forwardRef((props, ref) => {
return (
<li className="Avatar">
<img
src={props.url}
ref={ref}
/>
</li>
)
});

Categories

Resources