How do I write test for hooks? - reactjs

I am making unit test for the component and also trying to make a test for hook but I can't seem to get it working. This is my hook. What do I need to change or do to fix this test?
import { useState } from 'react';
function UseToggleState (initialValue = false) {
const [state, setState] = useState(initialValue);
const toggle = () => setState(!state);
return [state, toggle];
};
export default UseToggleState
And this is the component I am using it.
export function Todo({ id, task, completed }) {
const classes = useStyles();
const dispatch = useContext(DispatchContext);
const [isEditing, toggle] = useToggleState(false);
if (isEditing) {
return (
<li
className={classes.Todo}
style={{ overflowY: "hidden" }}
onClick={() => toggle()}
>
<EditForm id={id} task={task} toggleEditForm={toggle} />
</li>
);
}
return (
<li
className={classes.Todo}
onClick={() => dispatch({ type: TOGGLE_TODO, id })}
>
<span
style={{
textDecoration: completed ? "line-through" : "",
color: completed ? "#A9ABAE" : "#34495e",
}}
>
{task}
</span>
<div className={classes.icons}>
<FontAwesomeIcon
icon={faPen}
size="1x"
onClick={(e) => {
e.stopPropagation();
toggle();
}}
/>{" "}
<FontAwesomeIcon
icon={faTrash}
size="1x"
color={"#c0392b"}
onClick={(e) => {
e.stopPropagation();
dispatch({ type: REMOVE_TODO, id });
}}
/>
</div>
</li>
);
}
And the test file is as follows. It keeps saying that toggle is not a function and I am not quite sure why it is doing that. Is there something I need to change differently to make it work?
describe("useToggleState", () => {
it("Initial toggle is true", () => {
const { result } = renderHook(() => UseToggleState(true))
act(() => {
result.current.toggle
})
expect(result.current.state).toBeTruthy()
})
it("Toggle is false", () => {
const { result } = renderHook(() => UseToggleState(false))
act(() => {
result.current.toggle
})
expect(result.current.state).toBeFalsy()
})
})

You test is supposed to be:
describe("useToggleState", () => {
it("Initial toggle is false", () => {
const { result } = renderHook(() => UseToggleState(true))
act(() => {
result.current[1]()
})
expect(result.current[0]).toBe(false)
})
it("Toggle is true", () => {
const { result } = renderHook(() => UseToggleState())
act(() => {
result.current[1]()
})
expect(result.current[0]).toBe(true)
})
})

Related

getting error "Custom url is exist" in NextJS

I'm having a problem when I input a new url in the project's custom url.
So this url input can be edited, now when it can be edited I enter a new url behind "https://www.kutanya.com/share/", then I save and a notification "custom url is exist" appears.
my API =
export const setCustomUrl = (args, surveyId) => {
return new Promise((resolve, reject) => {
axios
.put(process.env.NEXT_PUBLIC_API_URL + `/v1/smart-survey/${surveyId}/custom-url`, args, {
headers: { Authorization: `Bearer ${token()}` },
})
.then((response) => {
resolve(response.data.data);
})
.catch((error) => {
console.log(error.response);
reject(error?.response?.data?.message || "Network error.");
});
});
};
file .jsx =
const ShareKey = ({ customUrl, surveyId, shareKey = "", setShareKey, updateUrl, refProps }) => {
const [isLoading, setIsLoading] = useState(false);
const [customInput, setCustomInput] = useState(process.env.NEXT_PUBLIC_SHARE_URL + "/");
const [active, setActive] = useState(false);
const [show, setShow] = useState(false);
const router = useRouter();
const getLink = () => {
setShareKey("");
setIsLoading(true);
automaticApproval({ surveyId })
.then((resolve) => {
console.log(resolve);
setShareKey(resolve.key);
})
.catch((reject) => {
console.log(reject);
toast.error(reject);
})
.finally(() => {
setIsLoading(false);
});
};
const toggleFunction = () => {
setActive(!active);
setShow(!show);
};
const SaveFunction = () => {
setCustomUrl({ customUrl }, router.query.surveyId)
.then((resolve) => {
console.log(resolve);
updateUrl({
customUrl
});
setShareKey(resolve.key);
toast.info("Berhasil ubah link");
})
.catch((reject) => {
console.log(reject);
toast.error(reject);
})
.finally(() => {
setIsLoading(false);
});
};
return (
<section>
<button onClick={getLink} disabled={isLoading} className={styles.link_button}>
{`GENERATE ${shareKey && "NEW "}LINK`}
</button>
{shareKey && (
<div className={styles.link_container}>
<label>Share link</label>
<div className={styles.link}>
<input
value={active ? customInput : process.env.NEXT_PUBLIC_SHARE_URL + "/" + shareKey}
onChange={(e) => {
setCustomInput(e.target.value);
}}
disabled={!active}
ref={refProps}
/>
{!show && (
<button
onClick={() => {
if (typeof navigator !== "undefined") {
navigator.clipboard.writeText(refProps.current.value);
toast.info("Copied to clipboard");
}
}}
>
copy
</button>
)}
</div>
{active ? (
<div className={styles.custom2}>
<button style={{ color: "red", marginTop: "6px" }} onClick={toggleFunction}>
Cancel
</button>
<button style={{ color: "blue", marginTop: "6px" }} onClick={SaveFunction}>
Save
</button>
</div>
) : (
<div className={styles.custom} onClick={toggleFunction}>
Create Custom URL
</div>
)}
</div>
)}
</section>
);
};
The question is, is there something wrong in my coding so that the output is 400 in the console and the notification appears?

How to show only the selected item in the modal

I am quite new to modals on react. I have 2 questions:
I am trying to show more details onclick the particular gif
what type of test can I write for the async/await response(I have never written a test before)
Further details:
react version: 18
async function handleSubmit(event) {
event.preventDefault();
await axios.get(`https://api.giphy.com/v1/gifs/search?q=${gifname}&api_key=3ZT8IGYuq0IQP1v19SAGm1RNkL5L5FUI`)
.then((response) => {
let resp = response.data.data
setgif(resp)
})
};
useEffect (() => {
handleSubmit()}, []
)
function GifList(props) {
const gif_use = props.gif;
const [modalIsOpen, setModalIsOpen] = useState(false);
return (
<>
<div className="img_div row">
{
gif_use.map((gif_use1, i) =>
<img className="col-lg-4" src={gif_use1.images.downsized.url}
alt='gif-result'
onClick={
() => setModalIsOpen(true)
}
key={i}/>
)}
</div>
<Modal isOpen={modalIsOpen} onRequestClose={
() => setModalIsOpen(false)
}>
{
gif_use.map((gif_use1, i) =>
<img className="col-lg-4" src={gif_use1.images.downsized.url}
alt='gif-result'
onClick={
() => setModalIsOpen(true)
}
key={i}/>
)}
<button onClick={
() => setModalIsOpen(false)
}>close</button>
</Modal>
</>
);
}

I'm using redux and firebase for storage and I don't know what I'm doing wrong

Actions/index.js The problem is in the PostArticleAPI() function
import db, { auth, provider, storage } from "../firebase";
import { SET_USER } from "./actionType";
export const setUser = (payload) => ({
type: SET_USER,
user: payload
});
export function signInAPI() {
return (dispatch) => {
auth.signInWithPopup(provider).then((payload) => {
console.log(payload);
dispatch(setUser(payload.user));
}).catch((error) => alert(error.message));
}
}
export function getUserAuth() {
return (dispatch) => {
auth.onAuthStateChanged(async (user) => {
if (user) {
dispatch(setUser(user));
}
})
}
}
export function signOutAPI() {
return (dispatch) => {
auth.signOut().then(() => {
dispatch(setUser(null));
}).catch((error) => {
console.error(error.message);
});
}
}
export function postArticleAPI(payload) {
return (dispatch) => {
if (payload.image != '') {
const upload = storage.ref(`images/${payload.image.name}`).put(payload.image);
upload.on('state_changed', (snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log(`Progress: ${progress}%`);
if (snapshot.state === 'RUNNING') {
console.log(`Progress: ${progress}%`);
}
}, (error) => console.log(error.code),
async () => {
const downloadURL = await upload.snapshot.ref.getDownloadURL();
db.collection('articles').add({
actor: {
description: payload.user.email,
title: payload.user.displayName,
date: payload.timestamp,
image: payload.user.photoURL
},
video: payload.video,
shareImg: downloadURL,
comments: 0,
description: payload.description,
})
}
);
}
}
}
PostModal.js
import React, { useState } from 'react'
import styled from 'styled-components';
import ReactPlayer from 'react-player';
import { connect } from 'react-redux';
import { postArticleAPI } from '../actions';
import firebase from 'firebase';
const PostModal = (props) => {
const [editorText, setEditorText] = useState('');
const [shareImage, setShareImage] = useState('');
const [videoLink, setVideoLink] = useState('');
const [assetArea, setAssetArea] = useState('');
const handleChange = (e) => {
const image = e.target.files[0];
if (image === '' || image === undefined) {
alert(`not an image, the file is a ${typeof (image)}`);
return;
}
setShareImage(image);
}
const switchAssetArea = (area) => {
setShareImage("");
setVideoLink("");
setAssetArea(area);
};
const postArticle = (e) => {
console.log("heyy abay");
e.preventDefault();
if (e.target !== e.currentTarget) {
console.log("heyy abay2");
return;
}
const payload = {
image: shareImage,
video: videoLink,
user: props.user,
description: editorText,
timestamp: firebase.firestore.Timestamp.now(),
}
props.postArticle(payload);
reset(e)
}
const reset = (e) => {
setEditorText("");
setShareImage("");
setVideoLink("");
setAssetArea("");
props.handleClick(e);
};
return (
<>
{props.showModal === "open" && (
<Container>
<Content>
<Header>
<h2>Create a post</h2>
<button onClick={(e) => reset(e)}>
<img src="/images/cancel.svg" alt="cancel" />
</button>
</Header>
<SharedContent>
<UserInfo>
{props.user && props.user.photoURL ? (
<img src={props.user.photoURL} alt="" />)
: (<img src="/images/user.svg" alt="" />)}
{/* <img src="/images/user.svg" alt="" /> */}
<span>{props.user.displayName}</span>
</UserInfo>
<Editor>
<textarea value={editorText}
onChange={(e) => setEditorText(e.target.value)}
placeholder='What do you want to talk about?'
autoFocus={true}
/>
{assetArea === "image" ? (
<UploadImage>
<input type="file"
accept='image/gif, image/jpeg, image/png'
name='image'
id='file'
style={{ display: 'none' }}
onChange={handleChange}
/>
<p>
<label htmlFor="file">Select an image to share</label>
</p>
{shareImage && <img
src={URL.createObjectURL(shareImage)} />}
</UploadImage>) : (
assetArea === "media" && (
<>
<input
type="text"
placeholder="Please input a video link"
value={videoLink}
onChange={e => setVideoLink(e.target.value)}
/>
{videoLink && (<ReactPlayer width={"100%"}
url={videoLink} />)}
</>))
}
</Editor>
</SharedContent>
<ShareCreations>
<AttachAssets>
<AssetButton onClick={() => switchAssetArea("image")} >
<img src="/images/gallery.svg" alt="" />
</AssetButton>
<AssetButton onClick={() => switchAssetArea("media")}>
<img src="/images/video.svg" alt="" />
</AssetButton>
</AttachAssets>
<ShareComment>
<AssetButton>
<img src="/images/chat1.svg" alt="" />
Anyone
</AssetButton>
</ShareComment>
<PostButton disabled={!editorText ? true : false}
onClick={(e) => postArticle(e)}>
Post
</PostButton>
</ShareCreations>
</Content>
</Container>)
}
</>
)
};
//I'm hiding CSS
const mapStateToProps = (state) => {
return {
user: state.userState.user,
};
}
const mapDispatchToProps = (dispatch) => ({
postArticle: (payload) => dispatch(postArticleAPI(payload)),
});
export default connect(mapStateToProps, mapDispatchToProps)(PostModal);
Error:
TypeError: firebase__WEBPACK_IMPORTED_MODULE_0_.storage.ref is not a function
This is the error I'm getting and in this line actions/index.js
const upload = storage.ref(`images/${payload.image.name}`).put(payload.image);
It is saying storage.ref is not a function. I am getting this error that is why I'm writing this extra lines
You are probably not exporting properly the storage from ../firebase.
If you share the code from that file we can help you more. Maybe it's also a missmatch with the new Firebase API version (v9) and old one (v8).

React Hooks clickhandler to children

Hello I have a clickhandler that I send to a child component and use it on onclick, but for some reason, my click handler event on my parent component is not running
parent jsx:
type ClickHandler = (tag: ITag) => (e: MouseEvent) => void
const MenuTags: React.FC<{hover: boolean}> = observer(({hover}) => {
const {layoutStore} = useRootStore()
const [tags, setTags] = useState<ITag[]>(Tags)
const showHideDropItem: ShowHideDropItem = (tag) => {
console.log(tag)
setTags((items) =>
items.map((item) => ({
...item,
Active: item.Name === tag.Name ? tag.Active !== true : false,
})),
)
}
const clickHandler: ClickHandler = (tag) => (e) => {
console.log('a')
e.preventDefault()
showHideDropItem(tag)
}
return (
<MenuList
open={layoutStore.sideBar || layoutStore.onHoverSideState}
hover={hover}
>
{tags.map((item) => (
<div key={JSON.stringify(item.Name)}>
{item.Title ? <div className="title_tagList">{item.Title}</div> : ''}
<TagList
open={layoutStore.sideBar || layoutStore.onHoverSideState}
tag={item}
clickHandler={clickHandler}
/>
</div>
))}
</MenuList>
)
})
my children jsx:
const TagList: React.FC<ITagList> = observer(({tag, clickHandler, open}) => {
const tagHandleClick = (e: any) => {
e.preventDefault()
if (tag.Active !== undefined) clickHandler(tag)
}
return (
<ListItem open={open} isDropDown={!!tag.DropdownItems} active={tag.Active}>
<div className="tag-container">
<NavLink
className="tag-wrapper"
to={tag.Link}
onClick={tagHandleClick}
>
<tag.Icon className="svg-main" size={22} />
<span className="tag-name">{tag.Name}</span>
</NavLink>
</div>
</ListItem>
)
})
when clicking on my event it enters my handler of the child component, but the handler does not call my parent component's handler
Your clickHandler is a function that returns a function. It might be easier to see if you temporarily rewrite it like this:
const clickHandler: ClickHandler = (tag) => {
return (e) => {
console.log("a")
e.preventDefault()
showHideDropItem(tag)
}
}
Instead of returning a function you could just do the logic of the inner function directly instead.
const clickHandler: ClickHandler = (tag) => {
console.log('a')
showHideDropItem(tag)
}

Conflicts between useEffect in react

I have to create component which fetch data with pagination and filters.
Filters are passed by props and if they changed, component should reset data and fetch it from page 0.
I have this:
const PaginationComponent = ({minPrice, maxPrice}) => {
const[page, setPage] = useState(null);
const[items, setItems] = useState([]);
const fetchMore = useCallback(() => {
setPage(prevState => prevState + 1);
}, []);
useEffect(() => {
if (page === null) {
setPage(0);
setItems([]);
} else {
get(page, minPrice, maxPrice)
.then(response => setItems(response));
}
}, [page, minPrice, maxPrice]);
useEffect(() => {
setPage(null);
},[minPrice, maxPrice]);
};
.. and there is a problem, because first useEffect depends on props, because I use them to filtering data and in second one I want to reset component. And as a result after changing props both useEffects run.
I don't have more ideas how to do it correctly.
In general the key here is to move page state up to the parent component and change the page to 0 whenever you change your filters. You can do it either with useState, or with useReducer.
The reason why it works with useState (i.e. there's only one rerender) is because React batches state changes in event handlers, if it didn't, you'd still end up with two API calls. CodeSandbox
const PaginationComponent = ({ page, minPrice, maxPrice, setPage }) => {
const [items, setItems] = useState([]);
useEffect(() => {
get(page, minPrice, maxPrice).then(response => setItems(response));
}, [page, minPrice, maxPrice]);
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.id}, {item.name}, ${item.price}
</div>
))}
<div>Page: {page}</div>
<button onClick={() => setPage(v => v - 1)}>back</button>
<button onClick={() => setPage(v => v + 1)}>next</button>
</div>
);
};
const App = () => {
const [page, setPage] = useState(0);
const [minPrice, setMinPrice] = useState(25);
const [maxPrice, setMaxPrice] = useState(50);
return (
<div>
<div>
<label>Min price:</label>
<input
value={minPrice}
onChange={event => {
const { value } = event.target;
setMinPrice(parseInt(value, 10));
setPage(0);
}}
/>
</div>
<div>
<label>Max price:</label>
<input
value={maxPrice}
onChange={event => {
const { value } = event.target;
setMaxPrice(parseInt(value, 10));
setPage(0);
}}
/>
</div>
<PaginationComponent minPrice={minPrice} maxPrice={maxPrice} page={page} setPage={setPage} />
</div>
);
};
export default App;
The other solution is to use useReducer, which is more transparent, but also, as usual with reducers, a bit heavy on the boilerplate. This example behaves a bit differently, because there is a "set filters" button that makes the change to the state that is passed to the pagination component, a bit more "real life" scenario IMO. CodeSandbox
const PaginationComponent = ({ tableConfig, setPage }) => {
const [items, setItems] = useState([]);
useEffect(() => {
const { page, minPrice, maxPrice } = tableConfig;
get(page, minPrice, maxPrice).then(response => setItems(response));
}, [tableConfig]);
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.id}, {item.name}, ${item.price}
</div>
))}
<div>Page: {tableConfig.page}</div>
<button onClick={() => setPage(v => v - 1)}>back</button>
<button onClick={() => setPage(v => v + 1)}>next</button>
</div>
);
};
const tableStateReducer = (state, action) => {
if (action.type === "setPage") {
return { ...state, page: action.page };
}
if (action.type === "setFilters") {
return { page: 0, minPrice: action.minPrice, maxPrice: action.maxPrice };
}
return state;
};
const App = () => {
const [tableState, dispatch] = useReducer(tableStateReducer, {
page: 0,
minPrice: 25,
maxPrice: 50
});
const [minPrice, setMinPrice] = useState(25);
const [maxPrice, setMaxPrice] = useState(50);
const setPage = useCallback(
page => {
if (typeof page === "function") {
dispatch({ type: "setPage", page: page(tableState.page) });
} else {
dispatch({ type: "setPage", page });
}
},
[tableState]
);
return (
<div>
<div>
<label>Min price:</label>
<input
value={minPrice}
onChange={event => {
const { value } = event.target;
setMinPrice(parseInt(value, 10));
}}
/>
</div>
<div>
<label>Max price:</label>
<input
value={maxPrice}
onChange={event => {
const { value } = event.target;
setMaxPrice(parseInt(value, 10));
}}
/>
</div>
<button
onClick={() => {
dispatch({ type: "setFilters", minPrice, maxPrice });
}}
>
Set filters
</button>
<PaginationComponent tableConfig={tableState} setPage={setPage} />
</div>
);
};
export default App;
You can use following
const fetchData = () => {
get(page, minPrice, maxPrice)
.then(response => setItems(response));
}
// Whenever page updated fetch new data
useEffect(() => {
fetchData();
}, [page]);
// Whenever filter updated reseting page
useEffect(() => {
const prevPage = page;
setPage(0);
if(prevPage === 0 ) {
fetchData();
}
},[minPrice, maxPrice]);

Resources