Unit testing a memo component with React hooks library - javascript

Am trying to perform unit tests with React hooks, but I haven't got a seamless approach, am using this library react hooks testing library, and I want to ensure that I test a component but wrapped with a memo, inside it there are useCallbacks, I want to test them one by one, the component below is exported like this export default memo(DateTimePicker); so how can I go deeper and test useCallback functions e.g getValueFormat, setFormatter etc.
Below is the component I want to test:
const DateTimePicker = ({
inputRefProp,
options = undefined,
datepicker = true,
timepicker = true,
placeholder = '',
defaultValue = '',
value: propValue = '',
displayDateFormat = 'DD.MM.YYYY',
displayTimeFormat = 'HH=mm',
displayDateTimeFormat = 'DD.MM.YYYY HH=mm',
value_format = undefined,
onChange = undefined,
onBlur = undefined,
onBlurTyped = undefined,
scrollInput = false,
customInput = undefined,
locale = 'de',
maxDate = undefined,
earlierDate = false,
}: Props) => {
const [value, setValue] = useState<ValueType>('');
const [isInvalid, setIsInvalid] = useState(false);
const [inputValue, setInputValue] = useState('');
const [selected, setSelected] = useState(false);
const inputRef = useRef(null);
const getDisplayFormat = useCallback(() => {
if (datepicker && timepicker) {
return displayDateTimeFormat;
}
if (datepicker) {
return displayDateFormat;
}
if (timepicker) {
return displayTimeFormat;
}
return displayDateTimeFormat;
}, [displayDateFormat, displayTimeFormat, displayDateTimeFormat, datepicker, timepicker]);
const getValueFormat = useCallback(() => {
if (value_format) {
return value_format;
}
if (datepicker && timepicker) {
return ISO_DATETIME_FORMAT;
}
if (datepicker) {
return ISO_DATE_FORMAT;
}
if (timepicker) {
return ISO_TIME_FORMAT;
}
return ISO_DATETIME_FORMAT;
}, [datepicker, timepicker, value_format]);
const setFormatter = useCallback(() => {
$.datetimepicker.setDateFormatter({
parseDate(date: Date, _format: string) {
const d = moment.utc(date, _format);
return d.isValid() ? d.toDate() : false;
},
formatDate(date: Date, _format: string) {
return moment.utc(date).format(_format);
},
});
}, []);
const getValue = useCallback(
(newValue: ValueType = '') => {
if (newValue) {
return moment.utc(newValue, getValueFormat()).format(getDisplayFormat());
}
return moment.utc(value, getValueFormat()).format(getDisplayFormat());
},
[getDisplayFormat, getValueFormat, value],
);
const onChangeHandler = useCallback(
(newValue: string) => {
setSelected(true);
let currenIsInvalid = false;
if (newValue) {
try {
const momentValue = moment.utc(newValue, true);
if (!momentValue.isValid()) {
currenIsInvalid = true;
} else {
const newInputValue = moment.utc(newValue).format(getDisplayFormat());
setInputValue(newInputValue || '');
if (onChange && newInputValue) {
onChange(moment.utc(newInputValue, getValueFormat()).toDate());
}
}
} catch (e) {
currenIsInvalid = true;
}
} else {
setInputValue(newValue);
if (onChange && newValue) {
onChange(moment.utc(newValue, getValueFormat()).toDate());
}
}
setIsInvalid(currenIsInvalid);
},
[getDisplayFormat, getValueFormat, onChange],
);
const setComputedValue = useCallback(
(momentValue: ValueType) => {
const newValue = momentValue && moment.utc(momentValue, getValueFormat()).toDate();
setInputValue(momentValue ? getValue(momentValue) : inputValue);
setValue(newValue);
const $input = $(inputRefProp.current);
if ($input) {
$input.datetimepicker('setOptions', { value: propValue });
}
},
[getValue, getValueFormat, inputValue, inputRefProp, propValue],
);
const initPlugin = useCallback(() => {
if (inputRefProp && inputRefProp.current) {
let $input = $(inputRefProp.current);
const inputCurrentValue = value || defaultValue;
const defaultOptions = {
formatTime: displayTimeFormat,
formatDate: displayDateFormat,
dayOfWeekStart: 1,
};
const pickerOptions = {
allowDates: [],
...defaultOptions,
...options,
};
const allowDateTimes = resolvePath(pickerOptions, 'allowDateTimes');
if (allowDateTimes) {
pickerOptions.allowDates = allowDateTimes.map((dt: moment.MomentInput) =>
moment.utc(dt, ISO_DATETIME_FORMAT).format(pickerOptions.formatDate),
);
}
setFormatter();
setInputValue('');
$input.datetimepicker('destroy');
$input = $input.datetimepicker({
...pickerOptions,
format: getDisplayFormat(),
datepicker,
timepicker,
onChangeDateTime: onChangeHandler,
value: inputCurrentValue,
scrollInput,
maxDate,
lazyInit: true,
});
}
// This function has to run only once since it initialize the datetimepicker plugin
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
$.datetimepicker.setLocale(locale);
initPlugin();
}, [initPlugin, locale]);
useEffect(() => {
if (propValue) {
if (propValue !== value) {
if (earlierDate) {
setComputedValue(new Date());
} else {
setComputedValue(propValue);
}
}
} else {
setInputValue('');
setValue('');
}
// This function should run whenever the propValue changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [propValue]);
const onBlurHandler = useCallback(
(newValue: ValueType) => {
if (onBlurTyped && !selected) {
if (maxDate) {
const compareDates = compareDesc(
newValue as Date,
moment(new Date(maxDate as string).getUTCDate(), getValueFormat()).toDate(),
);
if (!newValue || compareDates === 0 || compareDates === 1) onBlurTyped(newValue);
else onBlurTyped(value);
} else onBlurTyped(newValue);
}
if (onBlur) onBlur(newValue);
},
[onBlurTyped, onBlur, selected, getValueFormat, maxDate, value],
);
const onBlurInput = useCallback(() => {
if (isInvalid) {
setInputValue('');
setValue('');
onBlurHandler('');
} else {
if (!inputValue) {
setInputValue('');
setValue('');
onBlurHandler('');
return;
}
const newValue = inputValue && moment.utc(inputValue, getValueFormat()).toDate();
setComputedValue(inputValue);
onBlurHandler(newValue);
}
}, [getValueFormat, inputValue, isInvalid, onBlurHandler, setComputedValue]);
const onChangeHandlerInput = useCallback(
(newValue: string) => {
const re = /^[0-9]{0,2}\.?[0-9]{0,2}\.?[0-9]{0,4}$/;
let currentIsInvalid = false;
setSelected(false);
const validValue = re.test(newValue);
if (validValue) setInputValue(newValue);
if (newValue && validValue) {
try {
const momentValue = moment.utc(newValue, getDisplayFormat());
if (!momentValue.isValid()) {
currentIsInvalid = true;
}
} catch (e) {
currentIsInvalid = true;
}
} else {
currentIsInvalid = true;
}
setIsInvalid(currentIsInvalid);
},
[getDisplayFormat],
);
const onChangeInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChangeHandlerInput(e.target.value);
},
[onChangeHandlerInput],
);
const renderInput = useCallback(() => {
if (customInput) {
return React.cloneElement(customInput, {
inputRef,
placeholder,
value: inputValue,
onChange: onChangeInput,
onBlur: onBlurInput,
});
}
const inputEl = <input type="text" />;
return React.cloneElement(inputEl, {
inputRef,
placeholder,
value: inputValue,
onChange: onChangeInput,
onBlur: onBlurInput,
});
}, [customInput, inputValue, onBlurInput, onChangeInput, placeholder]);
return <div className="datetimepicker">{renderInput()}</div>;
};
export default memo(DateTimePicker);
EDIT :
This what I have regarding unit tests, but they aren't working as expected :
it('should test DateTimePicker default value. ', () =>{
const { result } = renderHook(() => <DateTimePicker/>)
expect(result.current.value).toBe('')
});

Related

Cannot get synced state in React INK when using "useStdin" hook with rawMode

I'm using ink package, and I'm building the following component:
MultiSelect.tsx file:
import React, { useEffect, useState } from 'react';
import { useStdin } from 'ink';
import type { ISelectItem, ISelectItemSelection } from './interfaces/select-item';
import { ARROW_DOWN, ARROW_UP, ENTER, SPACE } from './constants/input';
import MultiSelectView from './MultiSelect.view';
interface IProps {
readonly items: ISelectItem[];
readonly onSubmit: (selectedItems: string[]) => void;
}
const MultiSelect: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
const { stdin, setRawMode } = useStdin();
const [itemsSelectionState, setItemsSelectionState] = useState<ISelectItemSelection[]>(
props.items.map((item, index) => ({ ...item, selected: false, isHighlighted: index === 0 })),
);
const stdinInputHandler = (data: unknown) => {
const rawData = String(data);
if (rawData === ARROW_DOWN) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.isHighlighted = false;
if (highlightedIndex === prev.length - 1) {
clonedPrev[0]!.isHighlighted = true;
} else {
clonedPrev[highlightedIndex + 1]!.isHighlighted = true;
}
return clonedPrev;
});
}
if (rawData === ARROW_UP) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.isHighlighted = false;
if (highlightedIndex === 0) {
clonedPrev[prev.length - 1]!.isHighlighted = true;
} else {
clonedPrev[highlightedIndex - 1]!.isHighlighted = true;
}
return clonedPrev;
});
}
if (rawData === SPACE) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.selected = !prev[highlightedIndex]!.selected;
return clonedPrev;
});
}
if (rawData === ENTER) {
const selectedItemsValues = itemsSelectionState
.filter((item) => item.selected)
.map((item) => item.value);
props.onSubmit(selectedItemsValues);
}
};
useEffect(() => {
setRawMode(true);
stdin?.on('data', stdinInputHandler);
return () => {
stdin?.removeListener('data', stdinInputHandler);
setRawMode(false);
};
}, []);
return <MultiSelectView itemsSelection={itemsSelectionState} />;
};
export default MultiSelect;
MultiSelect.view.tsx file:
import { Box, Text } from 'ink';
import React from 'react';
import type { ISelectItemSelection } from './interfaces/select-item';
interface IProps {
readonly itemsSelection: ISelectItemSelection[];
}
const MultiSelectView: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
return (
<Box display="flex" flexDirection="column">
{props.itemsSelection.map((item) => (
<Box key={item.value} display="flex" flexDirection="row">
<Text color="blue">{item.isHighlighted ? '❯' : ' '}</Text>
<Text color="magenta">
{item.selected ? '◉' : '◯'}
</Text>
<Text color="white" bold={item.selected}>
{item.label}
</Text>
</Box>
))}
</Box>
);
};
export default MultiSelectView;
Then, when I use this component in my code:
const onSubmitItems = (items: string[]) => {
console.log(items);
};
render(<MultiSelect items={items} onSubmit={onSubmitItems} />);
the render function is imported from ink and items is something like [{value: 'x', label: 'x'}, {value:'y', label:'y'}]
When I hit the enter key, Then onSubmitItems is triggered, but it outputs empty list, although I did select some items...
For example, in this output:
I picked 3 items, but output is still empty list. And it does seem like the state changes, so why I don't get the updated state?
You can resolve the issue by using useCallback:
const stdinInputHandler = useCallback((data: Buffer) => {
const rawData = String(data);
if (rawData === ARROW_DOWN) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.isHighlighted = false;
if (highlightedIndex === prev.length - 1) {
clonedPrev[0]!.isHighlighted = true;
} else {
clonedPrev[highlightedIndex + 1]!.isHighlighted = true;
}
return clonedPrev;
});
}
if (rawData === ARROW_UP) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.isHighlighted = false;
if (highlightedIndex === 0) {
clonedPrev[prev.length - 1]!.isHighlighted = true;
} else {
clonedPrev[highlightedIndex - 1]!.isHighlighted = true;
}
return clonedPrev;
});
}
if (rawData === SPACE) {
setItemsSelectionState((prev) => {
const highlightedIndex = prev.findIndex((item) => item.isHighlighted);
if (highlightedIndex === -1) {
return prev;
}
const clonedPrev = structuredClone(prev);
clonedPrev[highlightedIndex]!.selected = !prev[highlightedIndex]!.selected;
return clonedPrev;
});
}
if (rawData === ENTER) {
const selectedItemsValues = itemsSelectionState
.filter((item) => item.selected)
.map((item) => item.value);
props.onSubmit(selectedItemsValues);
}
}, [itemsSelectionState]);
Then, in your useEffect:
useEffect(() => {
setRawMode(true);
stdin?.on('data', stdinInputHandler);
return () => {
stdin?.removeListener('data', stdinInputHandler);
setRawMode(false);
};
}, [stdinInputHandler]);
now useEffect will re-register the input handler with new functions locals up-to-date with the state

Each child in a list should have a unique "key" prop . Check the render method of `NewPost`

hello i am not sure why i getting this error massage can some one correct my code i can not find key in my new post components
this is my newpost.js code
import React, { useContext } from 'react';
import { useHttpClient } from '../../hooks/useHttpClient';
import useForm from '../../hooks/useForm';
import { AuthContext } from '../../context/auth';
import { useHistory } from 'react-router-dom/cjs/react-router-dom.min';
import { newPostForm } from '../../utils/formConfig';
import { appendData, renderRepeatedSkeletons } from '../../utils';
import ErrorModal from '../../components/Modal/ErrorModal';
import SkeletonElement from '../../components/Skeleton/SkeletonElement';
const NewPost = () => {
const auth = useContext(AuthContext);
const history = useHistory();
const { currentUser } = auth;
const { isLoading, sendReq, error, clearError } = useHttpClient();
const { renderFormInputs, renderFormValues, isFormValid } =
useForm(newPostForm);
const formValues = renderFormValues();
const formInputs = renderFormInputs();
const postSubmitHandle = async (evt) => {
evt.preventDefault(); //otherwise, there will be a reload
const formData = appendData(formValues);
formData.append('author', currentUser.userId);
try {
await sendReq(
`${process.env.REACT_APP_BASE_URL}/posts`,
'POST',
formData,
{
Authorization: `Bearer ${currentUser.token}`,
}
);
history.push('/');
} catch (err) {}
};
return (
<>
<ErrorModal error={error} onClose={clearError} />
{isLoading ? (
renderRepeatedSkeletons(<SkeletonElement type='text' />, 20)
) : (
<div className='container-create-page'>
<form className='form form__create'>
<h2>Create a new post</h2>
{formInputs}
<button
onClick={postSubmitHandle}
className='btn'
disabled={!isFormValid()}
>
Submit <span>→</span>
</button>
</form>
</div>
)}
</>
);
};
export default NewPost;
and this is my useform.js code
import { useState, useCallback } from 'react';
//"signupForm" => "formObj" (name, email, password) => "form"
const useForm = (formObj) => {
const [form, setForm] = useState(formObj);
const renderFormInputs = () => {
//renders an [] of <Input> for all input fields
return Object.values(form).map((inputObj) => {
const { value, label, errorMessage, valid, renderInput } = inputObj;
return renderInput(
onInputChange,
value,
valid,
errorMessage,
label,
onCustomInputChange
);
});
};
const renderFormValues = () => {
let values = {};
Object.keys(form).forEach((inputObj) => {
values[inputObj] = form[inputObj].value;
});
return values;
};
const isInputFieldValid = useCallback(
(inputField) => {
for (const rule of inputField.validationRules) {
if (!rule.validate(inputField.value, form)) {
inputField.errorMessage = rule.message;
return false;
}
}
return true;
},
[form]
);
const onInputChange = useCallback(
(event) => {
const { name, value } = event.target;
let inputObj = { ...form[name], value };
const isValidInput = isInputFieldValid(inputObj);
if (isValidInput && !inputObj.valid) {
inputObj = { ...inputObj, valid: true };
} else if (!inputObj.touched && !isValidInput && inputObj.valid) {
inputObj = { ...inputObj, valid: false };
}
inputObj = { ...inputObj, touched: true };
setForm({ ...form, [name]: inputObj });
},
[form, isInputFieldValid]
);
const onCustomInputChange = useCallback(
(type, value, InputIsValid) => {
setForm({
...form,
[type]: { ...form[type], value, valid: InputIsValid },
});
},
[form]
);
const isFormValid = useCallback(
(customForm) => {
let isValid = true;
const arr = Object.values(customForm || form);
for (let i = 0; i < arr.length; i++) {
if (!arr[i].valid) {
isValid = false;
break;
}
}
return isValid;
},
[form]
);
return {
renderFormInputs,
renderFormValues,
isFormValid,
setForm,
};
};
export default useForm;
submit button does not work . it just a simple form with submit button and 4 input value and one image . if some one need more information . please ask in the comment section
index.js contain renderRepeatedSkeletons
export const checkInArray = (arr, elem) => {
return arr && arr.indexOf(elem) !== -1;
};
export const canModifyComment = (currentUserId, authorId) =>
currentUserId === authorId;
export const canReply = (currentUserId) => !!currentUserId;
export const isReplying = (activeComment, commentId) =>
activeComment &&
activeComment.type === 'replying' &&
activeComment.id === commentId;
export const isEditing = (activeComment, commentId) =>
activeComment &&
activeComment.type === 'editing' &&
activeComment.id === commentId;
export const readingTime = (body) => {
const wpm = 225;
const words = body.trim().split(/\s+/).length;
return `${Math.ceil(words / wpm)} min read`;
};
export const appendData = (data) => {
const formData = new FormData();
for (let [key, value] of Object.entries(data)) {
if (Array.isArray(value)) {
value = JSON.stringify(value);
}
formData.append(`${key}`, value);
}
return formData;
};
export const getReplies = (comments, commentId) => {
return (
comments &&
comments
.filter((comment) => comment && comment.parentId === commentId)
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
);
};
export const formatDate = (date) => {
const options = { year: 'numeric', month: 'short', day: 'numeric' };
const today = new Date(date);
return today.toLocaleDateString('en-US', options);
};
export const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
};
export const renderRepeatedSkeletons = (element, count) => {
let skeletons = [];
for (let i = 0; i < count; i++) {
skeletons.push(element);
}
return skeletons;
};
export const renderAlternateSkeletons = (elementOne, elementTwo, count) => {
let skeletons = [];
for (let i = 0; i < count; i++) {
if (i % 2 === 0) {
skeletons.push(elementOne);
} else {
skeletons.push(elementTwo);
}
}
return skeletons;
};
Your renderRepeatedSkeletons function have to add a key to elements
export const renderRepeatedSkeletons = (element, count) => {
let skeletons = [];
for (let i = 0; i < count; i++) {
skeletons.push(<React.Fragment key={i} />{element}</React.Fragment>);
}
return skeletons;
};

Property "handle" does not exist on type "undefined" - react context and typescript

I'm converting my app from JS to TS. Everything has been working good under JS but when started conversion to TS I'm getting plenty of errors with handle functions like for example handleVideoAdd. Does anyone has idea what am I'm doing wrong? Tried many things without success...
Property 'handleVideoAdd' does not exist on type 'undefined'. TS2339 - and it's pointing out to this fragment of code:
const { handleVideoAdd, inputURL, handleInputURLChange } = useContext(Context)
My code looks like that:
Header.tsx
import { Context } from "../Context";
import React, { useContext } from "react";
import { Navbar, Button, Form, FormControl } from "react-bootstrap";
export default function Header() {
const { handleVideoAdd, inputURL, handleInputURLChange } =
useContext(Context);
return (
<Navbar bg="light" expand="lg">
<Navbar.Brand href="#home">Video App</Navbar.Brand>
<Form onSubmit={handleVideoAdd} inline>
<FormControl
type="text"
name="url"
placeholder="Paste url"
value={inputURL}
onChange={handleInputURLChange}
className="mr-sm-2"
/>
<Button type="submit" variant="outline-success">
Add
</Button>
</Form>
</Navbar>
);
}
Context.tsx
import { useEffect, useMemo, useState } from "react";
import { youtubeApi } from "./APIs/youtubeAPI";
import { vimeoApi } from "./APIs/vimeoAPI";
import React from "react";
import type { FormEvent } from "react";
const Context = React.createContext(undefined);
function ContextProvider({ children }) {
const [inputURL, setInputURL] = useState("");
const [videoData, setVideoData] = useState(() => {
const videoData = localStorage.getItem("videoData");
if (videoData) {
return JSON.parse(videoData);
}
return [];
});
const [filterType, setFilterType] = useState("");
const [videoSources, setVideoSources] = useState([""]);
const [wasSortedBy, setWasSortedBy] = useState(false);
const [showVideoModal, setShowVideoModal] = useState(false);
const [modalData, setModalData] = useState({});
const [showWrongUrlModal, setShowWrongUrlModal] = useState(false);
const createModalSrc = (videoItem) => {
if (checkVideoSource(videoItem.id) === "youtube") {
setModalData({
src: `http://www.youtube.com/embed/${videoItem.id}`,
name: videoItem.name,
});
} else {
setModalData({
src: `https://player.vimeo.com/video/${videoItem.id}`,
name: videoItem.name,
});
}
};
const handleVideoModalShow = (videoID) => {
createModalSrc(videoID);
setShowVideoModal(true);
};
const handleVideoModalClose = () => setShowVideoModal(false);
const handleWrongUrlModalShow = () => setShowWrongUrlModal(true);
const handleWrongUrlModalClose = () => setShowWrongUrlModal(false);
const handleInputURLChange = (e) => {
setInputURL(e.currentTarget.value);
};
const handleVideoAdd = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const source = checkVideoSource(inputURL);
if (source === "youtube") {
handleYouTubeVideo(inputURL);
} else if (source === "vimeo") {
handleVimeoVideo(inputURL);
} else {
handleWrongUrlModalShow();
}
};
const checkVideoSource = (inputURL) => {
if (inputURL.includes("youtu") || inputURL.length === 11) {
return "youtube";
} else if (inputURL.includes("vimeo") || inputURL.length === 9) {
return "vimeo";
}
};
const checkURL = (inputURL) => {
if (!inputURL.includes("http")) {
const properURL = `https://${inputURL}`;
return properURL;
} else {
return inputURL;
}
};
const checkInputType = (inputURL) => {
if (!inputURL.includes("http") && inputURL.length === 11) {
return "id";
} else if (!inputURL.includes("http") && inputURL.length === 9) {
return "id";
} else {
return "url";
}
};
const fetchYouTubeData = async (videoID) => {
const data = await youtubeApi(videoID);
if (data.items.length === 0) {
handleWrongUrlModalShow();
} else {
setVideoData((state) => [
...state,
{
id: videoID,
key: `${videoID}${Math.random()}`,
name: data.items[0].snippet.title,
thumbnail: data.items[0].snippet.thumbnails.medium.url, //default, medium, high
viewCount: data.items[0].statistics.viewCount,
likeCount: data.items[0].statistics.likeCount,
savedDate: new Date(),
favourite: false,
source: "YouTube",
url: inputURL,
},
]);
setInputURL("");
}
};
const handleYouTubeVideo = (inputURL) => {
const inputType = checkInputType(inputURL);
if (inputType === "id") {
fetchYouTubeData(inputURL);
} else {
const checkedURL = checkURL(inputURL);
const url = new URL(checkedURL);
if (inputURL.includes("youtube.com")) {
const params = url.searchParams;
const videoID = params.get("v");
fetchYouTubeData(videoID);
} else {
const videoID = url.pathname.split("/");
fetchYouTubeData(videoID[1]);
}
}
};
const fetchVimeoData = async (videoID) => {
const data = await vimeoApi(videoID);
if (data.hasOwnProperty("error")) {
handleWrongUrlModalShow();
} else {
setVideoData((state) => [
...state,
{
id: videoID,
key: `${videoID}${Math.random()}`,
name: data.name,
thumbnail: data.pictures.sizes[2].link, //0-8
savedDate: new Date(),
viewCount: data.stats.plays,
likeCount: data.metadata.connections.likes.total,
savedDate: new Date(),
favourite: false,
source: "Vimeo",
url: inputURL,
},
]);
setInputURL("");
}
};
const handleVimeoVideo = (inputURL) => {
const inputType = checkInputType(inputURL);
if (inputType === "id") {
fetchVimeoData(inputURL);
} else {
const checkedURL = checkURL(inputURL);
const url = new URL(checkedURL);
const videoID = url.pathname.split("/");
fetchVimeoData(videoID[1]);
}
};
const deleteVideo = (key) => {
let newArray = [...videoData].filter((video) => video.key !== key);
setWasSortedBy(true);
setVideoData(newArray);
};
const deleteAllData = () => {
setVideoData([]);
};
const toggleFavourite = (key) => {
let newArray = [...videoData];
newArray.map((item) => {
if (item.key === key) {
item.favourite = !item.favourite;
}
});
setVideoData(newArray);
};
const handleFilterChange = (type) => {
setFilterType(type);
};
const sourceFiltering = useMemo(() => {
return filterType
? videoData.filter((item) => item.source === filterType)
: videoData;
}, [videoData, filterType]);
const sortDataBy = (sortBy) => {
if (wasSortedBy) {
const reversedArr = [...videoData].reverse();
setVideoData(reversedArr);
} else {
const sortedArr = [...videoData].sort((a, b) => b[sortBy] - a[sortBy]);
setWasSortedBy(true);
setVideoData(sortedArr);
}
};
const exportToJsonFile = () => {
let dataStr = JSON.stringify(videoData);
let dataUri =
"data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
let exportFileDefaultName = "videoData.json";
let linkElement = document.createElement("a");
linkElement.setAttribute("href", dataUri);
linkElement.setAttribute("download", exportFileDefaultName);
linkElement.click();
};
const handleJsonImport = (e) => {
e.preventDefault();
const fileReader = new FileReader();
fileReader.readAsText(e.target.files[0], "UTF-8");
fileReader.onload = (e) => {
const convertedData = JSON.parse(e.target.result);
setVideoData([...convertedData]);
};
};
useEffect(() => {
localStorage.setItem("videoData", JSON.stringify(videoData));
}, [videoData]);
return (
<Context.Provider
value={{
inputURL,
videoData: sourceFiltering,
handleInputURLChange,
handleVideoAdd,
deleteVideo,
toggleFavourite,
handleFilterChange,
videoSources,
sortDataBy,
deleteAllData,
exportToJsonFile,
handleJsonImport,
handleVideoModalClose,
handleVideoModalShow,
showVideoModal,
modalData,
showWrongUrlModal,
handleWrongUrlModalShow,
handleWrongUrlModalClose,
}}
>
{children}
</Context.Provider>
);
}
export { ContextProvider, Context };
App.js (not converted to TS yet)
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import "bootstrap/dist/css/bootstrap.min.css";
import reportWebVitals from "./reportWebVitals";
import { ContextProvider } from "./Context.tsx";
ReactDOM.render(
<React.StrictMode>
<ContextProvider>
<App />
</ContextProvider>
</React.StrictMode>,
document.getElementById("root")
);
reportWebVitals();
This is because when you created your context you defaulted it to undefined.
This happens here: const Context = React.createContext(undefined)
You can't say undefined.handleVideoAdd. But you could theoretically say {}.handleVideoAdd.
So if you default your context to {} at the start like this: const Context = React.createContext({})
Your app shouldn't crash up front anymore.
EDIT: I see you're using TypeScript, in that case you're going to need to create an interface for your context. Something like this:
interface MyContext {
inputURL?: string,
videoData?: any,
handleInputURLChange?: () => void,
handleVideoAdd?: () => void,
deleteVideo?: () => void,
// and all the rest of your keys
}
Then when creating your context do this:
const Context = React.createContext<MyContext>(undefined);

Context state being updated unexpectedly - React Typescript

First time poster so let me know if more information is need.
Trying to figure out why my global state using context API is being updated even when my setSate method is commented out. I thought i might have been mutating the state directly accidently but I dont believe I am
"specialModes" in actionOnClick() is the state in question
const SpecialFunctions: FC = (props: Props) => {
const { currentModeContext, specialModesContext: specialActionsContext, performCalc, inputValueContext } = useContext(AppContext)
const { specialModes, setSpecialModes } = specialActionsContext
const { currentMode, setCurrentMode } = currentModeContext
const { inputValue, setInputValue } = inputValueContext
const categoryOnClick = (index: number) => {
setCurrentMode(specialModes[index])
console.log(specialModes[index].title);
}
const actionOnClick = (action: IAction) => {
let newAction = action
newAction.value = performCalc()
let newSpecialModes = specialModes.map((mode) => {
if (mode === currentMode) {
let newMode = mode
newMode.actions = mode.actions.map((element) => {
if (element === action) {
return newAction
}
else return element
})
return newMode
}
else return mode
})
//setSpecialModes(newSpecialModes)
}
let headings = specialModes.map((categorgy, index) => {
return <Heading isActive={categorgy === currentMode ? true : false} onClick={() => categoryOnClick(index)} key={index}>{categorgy.title}</Heading>
})
let actions = currentMode.actions.map((action, index) => {
return (
<Action key={index} onClick={() => actionOnClick(action)}>
<ActionTitle>{action.title}</ActionTitle>
<ActionValue>{action.value}</ActionValue>
</Action>
)
})
return (
<Wrapper>
<Category>
{headings}
</Category>
<ActionsWrapper toggleRadiusCorner={currentMode === specialModes[0] ? false : true}>
{actions}
</ActionsWrapper>
</Wrapper>
)
}
Context.tsx
interface ContextType {
specialModesContext: {
specialModes: Array<ISpecialModes>,
setSpecialModes: React.Dispatch<React.SetStateAction<ISpecialModes[]>>
},
currentModeContext: {
currentMode: ISpecialModes,
setCurrentMode: React.Dispatch<React.SetStateAction<ISpecialModes>>
},
inputValueContext: {
inputValue: string,
setInputValue: React.Dispatch<React.SetStateAction<string>>
},
inputSuperscriptValueContext: {
inputSuperscriptValue: string,
setInputSuperscriptValue: React.Dispatch<React.SetStateAction<string>>
},
performCalc: () => string
}
export const AppContext = createContext({} as ContextType);
export const ContextProvider: FC = ({ children }) => {
const [SpecialModes, setSpecialModes] = useState([
{
title: 'Rafter',
actions: [
{
title: 'Span',
active: false
},
{
title: 'Ridge Thickness',
active: false
},
{
title: 'Pitch',
active: false
}
],
},
{
title: 'General',
actions: [
{
title: 'General1',
active: false
},
{
title: 'General2',
active: false
},
{
title: 'General3',
active: false
}
],
},
{
title: 'Stairs',
actions: [
{
title: 'Stairs1',
active: false
},
{
title: 'Stairs2',
active: false
},
{
title: 'Stairs3',
active: false
}
],
}
] as Array<ISpecialModes>)
const [currentMode, setCurrentMode] = useState(SpecialModes[0])
const [inputValue, setInputValue] = useState('0')
const [inputSuperscriptValue, setInputSuperscriptValue] = useState('')
const replaceCharsWithOperators = (string: string): string => {
let newString = string.replaceAll(/\s/g, '') // delete white space
newString = newString.replace('×', '*')
newString = newString.replace('÷', '/')
console.log(string)
console.log(newString)
return newString
}
const performCalc = (): string => {
let originalEquation = `${inputSuperscriptValue} ${inputValue} =`
let equation = inputSuperscriptValue + inputValue
let result = ''
equation = replaceCharsWithOperators(equation)
result = eval(equation).toString()
setInputSuperscriptValue(`${originalEquation} ${result}`)
setInputValue(result)
console.log(result)
return result
}
return (
<AppContext.Provider value={
{
specialModesContext: {
specialModes: SpecialModes,
setSpecialModes: setSpecialModes
},
currentModeContext: {
currentMode,
setCurrentMode
},
inputValueContext: {
inputValue,
setInputValue
},
inputSuperscriptValueContext: {
inputSuperscriptValue,
setInputSuperscriptValue
},
performCalc
}}>
{children}
</AppContext.Provider>
)
}
In your mode.actions.map() function you are indirectly changing actions field of your original specialModes array.
To fix this problem you need to create shallow copy of specialModes array Using the ... ES6 spread operator.
const clonedSpecialModes = [...specialModes];
let newSpecialModes = clonedSpecialModes.map((mode) => {
// rest of your logic here
})

Converting Class app into Hooks for React Next app

I am going through documentation for Algolia and Next and am trying to get URLs to show up in the address bar, most of the examples are as Class Components but the app I am working on uses Hooks. I am trying to test some of the examples on my site, but am stuck on how to correctly convert a class app in React to hooks as I keep getting errors.
Class Example:
const updateAfter = 400;
const createURL = (state) => `?${qs.stringify(state)}`;
const searchStateToUrl = (props, searchState) =>
searchState ? `${props.location.pathname}${createURL(searchState)}` : '';
const urlToSearchState = ({ search }) => qs.parse(search.slice(1));
class App extends Component {
state = {
searchState: urlToSearchState(this.props.location),
lastLocation: this.props.location,
};
static getDerivedStateFromProps(props, state) {
if (props.location !== state.lastLocation) {
return {
searchState: urlToSearchState(props.location),
lastLocation: props.location,
};
}
return null;
}
onSearchStateChange = searchState => {
clearTimeout(this.debouncedSetState);
this.debouncedSetState = setTimeout(() => {
const href = searchStateToURL(searchState);
this.props.router.push(href, href, {
shallow: true
});
}, updateAfter);
this.setState({ searchState });
};
My converted attempt:
const createURL = state => `?${qs.stringify(state)}`;
const pathToSearchState = path =>
path.includes("?") ? qs.parse(path.substring(path.indexOf("?") + 1)) : {};
const searchStateToURL = searchState =>
searchState ? `${window.location.pathname}?${qs.stringify(searchState)}` : "";
const DEFAULT_PROPS = {
searchClient,
indexName: "instant_search"
};
const Page = () => {
const [searchState, setSearchState] = useState(<not sure what goes here>)
const [lastRouter, setRouterState] = useState(router)
Page.getInitialProps = async({ asPath }) => {
const searchState = pathToSearchState(asPath);
const resultsState = await findResultsState(App, {
...DEFAULT_PROPS,
searchState
});
return {
resultsState,
searchState
};
}
//unsure how to convert here
static getDerivedStateFromProps(props, state) {
if (!isEqual(state.lastRouter, props.router)) {
return {
searchState: pathToSearchState(props.router.asPath),
lastRouter: props.router
};
}
return null;
}
const onSearchStateChange = searchState => {
clearTimeout(debouncedSetState);
const debouncedSetState = setTimeout(() => {
const href = searchStateToURL(searchState);
router.push(href, href, {
shallow: true
});
}, updateAfter);
setSearchState({ searchState });
};

Categories