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 });
};
Related
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('')
});
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);
I have this code for my context provider, I have my wrapped in component but still when I try to use it in a child using either useProductState or useProductDispatch, it returns undefined (throws err);
import React from "react";
import productsReducer from "./productsReducer";
const ProductsStateContext = React.createContext();
const ProductsDispatchContext = React.createContext();
const initialState = {
restaurantTitle: "",
restaurantId: "VljSa5Eakepw9QkTAUOW",
productsCollection: "",
categories: [],
defaultCategory: "",
isLoading: true,
};
function ProductsProvider({ children }) {
const [state, dispatch] = React.useReducer(productsReducer, initialState);
return (
<ProductsStateContext.Provider value={state}>
<ProductsDispatchContext.Provider value={dispatch}>
{children}
</ProductsDispatchContext.Provider>
</ProductsStateContext.Provider>
);
}
function useProductsState() {
const context = React.useContext(ProductsStateContext);
if (context === undefined) {
throw new Error("useProductsState must be used within a ProductsProvider");
}
return context;
}
function useProductsDispatch() {
const context = React.useContext(ProductsDispatchContext);
if (context === undefined) {
throw new Error(
"useProductsDispatch must be used within a ProductsProvider"
);
}
return context;
}
export { ProductsProvider, useProductsState, useProductsDispatch };
Can somebody explain how this works, I'm trying to access state and dispatch into a functional component that is a child of .
UPDATE:
I've got this as an action for my reducer
case "FETCH_RESTAURANT_DATA": {
return fetchRestaurantData(state, action.payload);
}
Function body looks like this:
const fetchRestaurantData = (state, value) => {
let newState = state;
return axios
.post(api.routes.restaurant, { restaurantId: state.restaurantId })
.then((res) => {
newState.restaurantTitle = res.data.restaurantTitle;
res.data.categories.forEach(
(category) =>
(newState.categories[category] = {
loaded: false,
props: [],
})
);
newState.defaultCategory = res.data.categories[0];
newState.productsCollection = res.data.productsCollection;
newState.isLoading = false;
return axios.post(api.routes.category, {
productsCollection: res.data.productsCollection,
categoryId: newState.defaultCategory,
});
})
.then((res) => {
newState.categories[newState.defaultCategory].props =
res.data[newState.defaultCategory];
newState.categories[newState.defaultCategory].loaded = true;
console.log(newState);
return newState;
});
};
What i think is going on, I think in reducer it does not wait for my response and update context state with an undefined value which then triggers my error.
I have tried to make a middle async function that awaits for fetchRestaurantData() response but it is still updating before getting a response
You should wait for the response in fetchRestaurantData:
const fetchRestaurantData = async (state, value) => { // add async keyword to the function
let newState = state;
return await axios // here add await
.post(api.routes.restaurant, { restaurantId: state.restaurantId })
.then((res) => {
newState.restaurantTitle = res.data.restaurantTitle;
res.data.categories.forEach(
(category) =>
(newState.categories[category] = {
loaded: false,
props: [],
})
);
newState.defaultCategory = res.data.categories[0];
newState.productsCollection = res.data.productsCollection;
newState.isLoading = false;
return axios.post(api.routes.category, {
productsCollection: res.data.productsCollection,
categoryId: newState.defaultCategory,
});
})
.then((res) => {
newState.categories[newState.defaultCategory].props =
res.data[newState.defaultCategory];
newState.categories[newState.defaultCategory].loaded = true;
console.log(newState);
return newState;
});
};
More information about the async functions
I am simply looking to save and restore a search term(form data) when a page is refreshed/reloaded. I have tried several solutions to no avail.
Flow: A user submits a search term and is taken to Spotify to retrieve an accessToken, if it is not already available. The initial page is refreshed once the accessToken is retrieved, but the search must be re-entered. This is not good UX.
I concluded that Web Storage was they way to go, of course it is not the only route. I am not sure if this is something that should be relegated to Lifecycle methods: componentDidMount() & componentDidUpdate(). Perhaps that is overkill? In any event, I attempted to employ both localStorage and sessionStorage. My implementation is obviously off as I am not getting the expected result. React dev tools displays the state of the SearchBar term, but it is not being saved. Also of note is the following: React dev tools shows that the onSubmit event handler is registering as bound () {} instead of the expected bound handleInitialSearchTerm() {}. The console also shows that there are no errors.
No third-party libraries please.
SearchBar.js
import React from 'react';
import "./SearchBar.css";
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
term: this.handleInitialSearchTerm
};
this.search = this.search.bind(this);
this.handleInitialSearchTerm = this.handleInitialSearchTerm.bind(this);
this.setSearchTerm = this.setSearchTerm.bind(this);
this.handleSearchOnEnter = this.handleSearchOnEnter.bind(this);
this.handleTermChange = this.handleTermChange.bind(this);
}
handleInitialSearchTerm = (event) => {
if (typeof (Storage) !== "undefined") {
if (localStorage.term) {
return localStorage.term
} else {
return this.setSearchTerm(String(window.localStorage.getItem("term") || ""));
}
}
};
setSearchTerm = (term) => {
localStorage.setItem("term", term);
this.setState({ term: term });
}
search() {
this.props.onSearch(this.state.term);
}
handleSearchOnEnter(event) {
if (event.keyCode === 13) {
event.preventDefault();
this.search();
}
}
handleTermChange(event) {
this.setState({
term: event.target.value
});
}
render() {
return (
<div className="SearchBar">
<input
placeholder="Enter A Song, Album, or Artist"
onChange={this.handleTermChange}
onKeyDown={this.handleSearchOnEnter}
onSubmit={this.handleInitialSearchTerm}
/>
<button className="SearchButton" onClick={this.search}>
SEARCH
</button>
</div>
);
}
}
export default SearchBar;
Motify.js
let accessToken;
const clientId = "SpotifyCredentialsHere";
const redirectUri = "http://localhost:3000/";
const CORS = "https://cors-anywhere.herokuapp.com/"; // Bypasses CORS restriction
const Motify = {
getAccessToken() {
if (accessToken) {
return accessToken;
}
// if accessToken does not exist check for a match
const windowURL = window.location.href;
const accessTokenMatch = windowURL.match(/access_token=([^&]*)/);
const expiresInMatch = windowURL.match(/expires_in=([^&]*)/);
if (accessTokenMatch && expiresInMatch) {
accessToken = accessTokenMatch[1]; //[0] returns the param and token
const expiresIn = Number(expiresInMatch[1]);
window.setTimeout(() => accessToken = "", expiresIn * 1000);
// This clears the parameters, allowing us to grab a new access token when it expires.
window.history.pushState("Access Token", null, "/");
return accessToken;
} else {
const accessUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectUri}`;
window.location = accessUrl;
}
},
search(term) {
const accessToken = Motify.getAccessToken();
const url = `${CORS}https://api.spotify.com/v1/search?type=track&q=${term}`;
return fetch(url, { headers: { Authorization: `Bearer ${accessToken}` }
}).then(response => response.json()
).then(jsonResponse => {
if (!jsonResponse.tracks) {
return [];
}
return jsonResponse.tracks.items.map(track => ({
id: track.id,
name: track.name,
artist: track.artists[0].name,
album: track.album.name,
uri: track.uri,
preview_url: track.preview_url
}));
})
}
...
Please check the code I have added.
Changes I did are below:
1)
this.state = {
term: JSON.parse(localStorage.getItem('term')) || '';
};
setSearchTerm = (term) => {
this.setState({
term: term
},
() => {
localStorage.setItem('term', JSON.stringify(this.state.term)));
}
import React from 'react';
import "./SearchBar.css";
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = {
term: JSON.parse(localStorage.getItem('term')) || '';
};
this.search = this.search.bind(this);
this.handleInitialSearchTerm = this.handleInitialSearchTerm.bind(this);
this.setSearchTerm = this.setSearchTerm.bind(this);
this.handleSearchOnEnter = this.handleSearchOnEnter.bind(this);
this.handleTermChange = this.handleTermChange.bind(this);
}
handleInitialSearchTerm = (event) => {
if (typeof(Storage) !== "undefined") {
if (localStorage.term) {
return localStorage.term
} else {
return this.setSearchTerm(String(window.localStorage.getItem("term") || ""));
}
}
};
setSearchTerm = (term) => {
this.setState({
term: term
},
() => {
localStorage.setItem('term', JSON.stringify(this.state.term)));
}
search() {
this.props.onSearch(this.state.term);
}
handleSearchOnEnter(event) {
if (event.keyCode === 13) {
event.preventDefault();
this.search();
}
}
handleTermChange(event) {
this.setState({
term: event.target.value
});
}
render() {
return ( <
div className = "SearchBar" >
<
input placeholder = "Enter A Song, Album, or Artist"
onChange = {
this.handleTermChange
}
onKeyDown = {
this.handleSearchOnEnter
}
onSubmit = {
this.handleInitialSearchTerm
}
/> <
button className = "SearchButton"
onClick = {
this.search
} >
SEARCH <
/button> <
/div>
);
}
}
export default SearchBar;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.1/umd/react-dom.production.min.js"></script>
If it is in hooks i would have done like below:
import React, {
useEffect,
useState,
useRef,
} from 'react';
function App() {
const [value, setValue] = useState(() => {
if (localStorage.getItem('prevCount') === null) {
return 0;
} else {
return localStorage.getItem('prevCount');
}
});
const countRef = useRef();
useEffect(() => {
countRef.current = value;
if (countRef.current) {
localStorage.setItem('prevCount', countRef.current);
} else {
localStorage.setItem('prevCount', 0);
}
});
const handleIncrement = () => {
setValue((value) => +value + 1);
};
const handleDecrement = () => {
if (value === 0) {
return;
} else {
setValue((value) => value - 1);
}
};
return (
<div className="card">
<label className="counterLabel">Simple Counter</label>
<button
className="button"
onClick={handleIncrement}
>
Increment
</button>
<span className="count">{value}</span>
<button
className="button"
onClick={handleDecrement}
>
Decrement
</button>
</div>
);
}
export default App;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.1/umd/react-dom.production.min.js"></script>
So what the above code is doing is that when we inititalize the state value we first check the localStorage , if "term" has value in localStorage we will use that value or else an empty string is initialized.
Using callback of setState inside the method setSearchTerm we set the term value immediately
Try the useLocalStorage hook to save search client side.
// useLocalStorage Hook to persist values client side
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};
return [storedValue, setValue];
}
credit: Brandon Baars
I have the following data structure in firebase as a realtime database:
{
"react" : {
"url_01" : "https://stackoverflow.com/",
"url_02" : "https://google.com/",
"url_03" : "https://www.youtube.com/"
}
}
I'm trying to query the database in React to display all URLs in the below component.
So far I got it to display the first URL in the database correctly but now trying to display them all in the div as <h1>.
class FirebaseDB extends React.Component {
constructor() {
super();
this.state = {
speed: [],
};
}
componentDidMount() {
const rootRef = firebase.database().ref().child('react');
const speedRef = rootRef.child('url_01');
speedRef.on('value', snap => {
this.setState({
speed: snap.val()
});
});
}
render() {
return (
<div>
<h1>URL: {this.state.speed}</h1>
</div>
);
}
}
componentDidMount() {
const rootRef = firebase.database().ref();
const speedRef = rootRef.child('react');
speedRef.once("value", snap => {
// Handle state
let speedsUrls = []
snap.forEach(child => {
speedsUrls.push(child.val())
});
this.setState({speed: speedsUrls})
});
}
render() {
const SpeedURLS = this.state.speed.map(url => <h1>URL: {url}</h1>);
return (
<div>
{SpeedURLS}
</div>
);
}
Another solution:
const object1 = {
"url_01" : "https://stackoverflow.com/",
"url_02" : "https://google.com/",
"url_03" : "https://www.youtube.com/"
};
let a = Object.values(object1);
a is now
["https://stackoverflow.com/","https://google.com/","https://www.youtube.com/"]