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
Related
i was doing a todo list app on React, and, tring to handle the changes i got the following error:
Uncaught TypeError: prevTodos is not iterable
My handle function:
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
setTodos((prevTodos) => {
return [...prevTodos, { id: v4(), name: name, complete: false }];
});
todoNameRef.current.value = null;
}
Full Code:
import React, { useState, useRef, useEffect } from "react";
import TodoList from "./TodoList";
import { v4 } from "uuid";
const LOCAL_STORAGE_KEY = 'todoApp.todos';
function App() {
const [todos, setTodos] = useState([]);
const todoNameRef = useRef();
useEffect(() => {
const storedTodos = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedTodos) setTodos(storedTodos);
setTodos();
}, []);
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos));
}, [todos]);
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
setTodos((prevTodos) => {
return [...prevTodos, { id: v4(), name: name, complete: false }];
});
todoNameRef.current.value = null;
}
return (
<>
<TodoList todos={todos} />
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Complete</button>
<div>0 left to do</div>
</>
);
}
export default App;
This lines are the problem
useEffect(() => {
const storedTodos = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedTodos) setTodos(storedTodos);
setTodos();
}, []);
you should parse the value before setting and there is no need for that setTodos() without value, because of that you would later get "undefined" is not a valid JSON:
useEffect(() => {
const storedTodos = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedTodos) setTodos(JSON.parse(storedTodos));
}, []);
I haven't ran the code or tested anything but I think it's just the way your calling setTodos. Instead of passing in an anonymous function I would define the new todo list first, then use it to set the state. Try something like this.
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
const newTodos = [...prevTodos, { id: v4(), name: name, complete: false }];
setTodos(newTodos);
todoNameRef.current.value = null;
}
You could probably get away with this too.
function handleAddTodo(e) {
const name = todoNameRef.current.value;
if (name === '') return;
setTodos([...prevTodos, { id: v4(), name: name, complete: false }]);
todoNameRef.current.value = null;
}
Hopefully that helps.
I'm using checkbox in react and the theme I'm using provides a hook but when my page loads I get a problem error. I am using checkbox for multiple delete action of elements in the table. I do not have a problem with deletion, but it generates an error on page load due to the check value.
this is the structure i use
https://facit-modern.omtankestudio.com/components/table
the hook i use
import React, { useEffect, useRef } from 'react';
import { useFormik } from 'formik';
import { Checks } from '../components/theme';
const useSelectTable = (data) => {
const selectTable = useFormik({
initialValues: {
selectAll: false,
selectedList: [],
},
});
// Update Select List
useEffect(() => {
if (selectTable.values.selectAll) {
selectTable.setValues({
...selectTable.values,
selectedList: data.map((d) => d?.id?.toString()),
});
} else {
selectTable.setValues({
...selectTable.values,
selectedList: [],
});
}
return () => {};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectTable.values.selectAll]);
// Select All -- indeterminate
const ref = useRef(null);
useEffect(() => {
if (
!!selectTable.values.selectedList.length &&
selectTable.values.selectedList.length !== data.map((d) => d?.id?.toString()).length
) {
ref.current.checked = false;
ref.current.indeterminate = true;
} else if (
selectTable.values.selectedList.length === data.map((d) => d?.id?.toString()).length
) {
ref.current.checked = true;
ref.current.indeterminate = false;
} else if (selectTable.values.selectedList?.length === 0) {
ref.current.checked = false;
ref.current.indeterminate = false;
}
}, [selectTable.values.selectAll, selectTable.values.selectedList, data]);
const SelectAllCheck = (
<Checks
ref={ref}
id='selectAll'
onChange={selectTable.handleChange}
checked={selectTable?.values?.selectAll}
/>
);
const selectItemHandleChange = selectTable.handleChange;
const selectedIdList = selectTable?.values?.selectedList;
return { selectTable, selectItemHandleChange, selectedIdList, SelectAllCheck };
};
export default useSelectTable;
usage:
const { selectTable, SelectAllCheck } = useSelectTable(slotData);
<td>
<Checks
id={slotAreas?.slot_number?.toString()}
name='selectedList'
value={slotAreas?.slot_number?.toString()}
onChange={selectTable?.handleChange}
checked={selectTable?.values?.selectedList.includes(
slotAreas?.slot_number?.toString(),
)}
/>
</td>
error javascript is a common error but I couldn't find a solution in react
can you help to solve this problem
I'm trying to fetch all objects from the favorites array and set the checkbox to checked
I've checked online and tried using the localStorage for that yet nothing works and the values aren't saved after refreshing.
Would appreciate any help!
Selected Book Component :
import React, { useEffect, useState } from 'react';
import { bookService } from '../service/book.service';
export const SelectedBook = ({ selectedBook, setFavorites, favorites, removeFavorite }) => {
const onHandleFavorite = (book, e) => {
if (e.currentTarget.checked) {
setFavorites([...favorites, book]);
bookService.addFavorite(book);
} else {
removeFavorite(book);
}
};
const isFavorite = () => {
if (!favorites.includes(selectedBook)) {
return false;
} else {
return true;
}
};
return (
<div className='selected-book-container'>
<input type='checkbox' checked={isFavorite()} onChange={(e) => onHandleFavorite(selectedBook, e)} />
<div className='title'>{selectedBook?.title}</div>
</div>
);
};
Book Page component :
import React, { useEffect, useState } from 'react';
import { bookService } from '../service/book.service.js';
import { BookList } from '../cmps/BookList';
import { SelectedBook } from '../cmps/SelectedBook.jsx';
import { utilService } from '../service/util.service';
export const BookPage = () => {
const [books, setBooks] = useState([]);
const [favorites, setFavorites] = useState([]);
const [index, setIndex] = useState(0);
const [selectedBook, setSelectedBook] = useState();
useEffect(() => {
bookService.favoriteQuery().then((res) => {
setFavorites(res);
});
}, []);
useEffect(() => {
bookService.query().then((res) => {
setBooks(res);
setSelectedBook(res[0]);
});
}, []);
document.onkeydown = checkKey;
function checkKey(e) {
e = e || window.event;
if (e.keyCode == '37') {
if (index === 0) return;
setIndex(index - 1);
} else if (e.keyCode == '39') {
if (index >= books.length - 1) return;
setIndex(index + 1);
}
}
useEffect(() => {
setSelectedBook(books[index]);
}, [index]);
const removeFavorite = (book) => {
setFavorites(favorites.filter((favorite) => favorite.id !== book.id));
bookService.removeFavorite(selectedBook);
};
return (
<div>
<div className='main-container main-layout'>
<div className='second'>
<SelectedBook
selectedBook={selectedBook}
setFavorites={setFavorites}
favorites={favorites}
removeFavorite={removeFavorite}
/>
<BookList books={favorites} removeFavorite={removeFavorite} />
</div>
</div>
<div className='footer-container'>
<section className='footer'>Footer</section>
</div>
</div>
);
};
Service :
async function favoriteQuery() {
try {
let favorites = await _loadeFavoriteFromStorage();
if (!favorites) return (favorites = []);
return favorites;
} catch (err) {
console.log('cannot load favorites', err);
}
}
function _loadeFavoriteFromStorage() {
return storageService.loadFromStorage(STORAGE_FAVORITE_KEY);
}
Storage Service :
export const storageService = {
loadFromStorage,
saveToStorage
}
function saveToStorage(key, val) {
localStorage.setItem(key, JSON.stringify(val))
}
function loadFromStorage(key) {
var val = localStorage.getItem(key)
return JSON.parse(val)
}
thanks for any kind of help
You're not updating localstorage each time that checked is being changed. You're calling setFavorites with a new set of favorites but this is just changing state. I would suggest creating a function within the book page component which does
function changeFavorite(book, checked){
saveToStorage(book?, checked)
rerender()
}
and having rerender set the state of favorites to whatever is in localstorage to ensure that you have a single source of truth which is found in localstorage and that you change that and not anything else
I'll just add
if (!favorites.includes(selectedBook)) {
return false;
} else {
return true;
}
};
Could really look like
const isFavorite () => favorites.includes(selectedBook)
I also didn't quite understand how you're doing about storing the books in object storage. You should probably have an id of some sorts which you use to save favorite information with
I am trying to clear localstorage with a button and an addEventListener. But its not working, and I cant figure out why. Thanks.
const clearStorage = document.querySelector(".clear-button");
clearStorage.addEventListener("click", (function(){
localStorage.clear();
}));
};
This code gets imported to the script below:
import { getFavourites } from "./utils/getFavs.js";
import createMenu from "./components/createMenu.js";
import displayMessage from "./components/displayMessage.js";
import { clearFavList } from "./components/clearFavList.js"
createMenu();
getFavourites();
const favouriteList = getFavourites();
const articlesContainer = document.querySelector(".favourites-container");
if(!favouriteList.length) {
displayMessage("error", "You don't have any saved favourites yet.", ".favourites-container");
}
favouriteList.forEach((favourite) => {
articlesContainer.innerHTML += `<div class="article">
<div class="article-content-text">
<h2 class="article-title fav-wrapper-text">Title: ${favourite.title}</h2>
</div>
<div>
<i class="fas fa-heart favButton"></i>
</div>
</div>`;
});
clearFavList(favouriteList);
This code, from a React auth component, have all the basic functions to handle storage.
// you can create multiple storage stores
const LOCAL_STORAGE_STORE = 'storage_sample';
export const getHasLocalStorageAuth = () => {
// check local storage
const localStorage = __getLocalStorage(LOCAL_STORAGE_STORE);
return { status: !!localStorage, data: localStorage.auth };
};
export const clearLocalStorageAuth = () => {
__clearLocalStorage(LOCAL_STORAGE_STORE);
return;
};
export const setLocalStorageAuth = (newLocalStorage: any) => {
__setLocalStorage(LOCAL_STORAGE_STORE, newLocalStorage);
return;
};
// setting data to localstorage
export function __setLocalStorage(
localStorageName: string,
localStorageValue: any,
isJson = true
) {
if (isJson) {
localStorage.setItem(localStorageName, JSON.stringify(localStorageValue));
} else {
localStorage.setItem(localStorageName, localStorageValue);
}
}
// getting data from localstorage
export function __getLocalStorage(localStorageName: string): any {
let localStorageValue: any;
if (localStorage.getItem(localStorageName) !== null) {
localStorageValue = localStorage.getItem(localStorageName);
} else {
localStorageValue = false;
}
return JSON.parse(localStorageValue);
}
// clear data from localstorage
export function __clearLocalStorage(localStorageName: string | null) {
localStorage.clear();
}
I am using redux to update an array of characters as a user types or erases it, so that when the user correctly types the entire phrase I can set a success flag.
So far when typing in characters the redux type SET_INPUT fires off and updates my state but unfortunately my REMOVE_INPUT doesn't seem to fire off but it does however reach the action.
My Reducer:
import { GET_PHRASE, SET_LOADING, SET_INPUT, REMOVE_INPUT } from "../types";
const initialState = {
level: 1,
phrase: null,
scrambledPhrase: null,
words: [],
input: [],
goal: [],
success: false,
loading: false,
};
export const phraseReducer = (state = initialState, action) => {
switch (action.type) {
case GET_PHRASE:
return {
...state,
phrase: action.payload.sentence,
scrambledPhrase: action.payload.scrambledPhrase,
words: action.payload.phrase.split(" "),
goal: action.payload.phrase.split(""),
loading: false,
};
case SET_INPUT:
console.log("setting input");
return {
...state,
input: [...state.input, action.payload],
};
case REMOVE_INPUT:
console.log("removing input");
return {
...state,
input: [...state.input.slice(0, -1)],
};
case SET_LOADING:
return {
...state,
loading: true,
};
default:
return state;
}
};
My Actions:
import { GET_PHRASE, SET_LOADING, SET_INPUT, REMOVE_INPUT } from "../types";
import axios from "axios";
export const getPhrase = (level) => async (dispatch) => {
try {
setLoading();
await axios
.get(`MY ROUTE`)
.then((res) => {
// console.log(res);
const sentence = res.data.data.phrase;
const scrambledSentence = scramblePhrase(
res.data.data.phrase
);
dispatch({
type: GET_PHRASE,
payload: {
phrase: phrase.toLowerCase(),
scrambledPhrase: scrambledPhrase.toLowerCase(),
},
});
});
} catch (err) {
console.error(err);
}
};
// SET INPUT
export const setInput = (input) => async (dispatch) => {
try {
dispatch({
type: SET_INPUT,
payload: input,
});
} catch (err) {
console.error(err);
}
};
// REMOVE INPUT
export const removeInput = () => {
try {
console.log("remove reached in actions");
return {
type: REMOVE_INPUT,
};
} catch (err) {
console.error(err);
}
};
// SET LOADING
export const setLoading = () => {
console.log("Loading...");
return {
type: SET_LOADING,
};
};
My Component to input a character:
import React, { useState } from "react";
// redux imports
import { connect } from "react-redux";
import { setInput, removeInput } from "../redux/actions/phraseActions";
import PropTypes from "prop-types";
const Character = ({ character, hasSpace, setInput }) => {
const [success, setSuccess] = useState();
const handleChange = (e) => {
if (e.target.value === character) {
// console.log("Success");
setSuccess(true);
} else {
setSuccess(false);
}
};
const keyedDown = (e) => {
// check for space or a letter
if (e.keyCode === 32 || (e.keyCode > 64 && e.keyCode < 91)) {
setInput(String.fromCharCode(e.keyCode).toLowerCase());
}
// check for backspace
else if (e.keyCode === 8) {
removeInput();
}
};
return (
<div
className={`character ${
success ? "success" : hasSpace ? "space" : ""
}`}
>
<input
type="text"
name="name"
required
maxLength="1"
size="1"
onChange={handleChange}
onKeyDown={keyedDown}
className="input"
autoComplete="off"
></input>
</div>
);
};
Character.propTypes = {
setInput: PropTypes.func.isRequired,
removeInput: PropTypes.func.isRequired,
};
const mapStateToProps = (state) => ({
// define state
phrase: state.phrase,
});
export default connect(mapStateToProps, { setInput, removeInput })(Character);
In my console you can see which points I am reaching:
In your event handler you are not calling removeInput that was provided by connect (props.removeInput) but the imported removeInput that doesn't dispatch anything and just returns an action object, so I suggest changing the component definition to:
const Character = ({ character, hasSpace, setInput, removeInput }) => {
In your reducer you can do: input: state.input.slice(0, -1), because slice already returns a shallow copy of the array so no need to copy it with [...]