How to merge similar custom hooks into one custom hooks - javascript

I have a situation where there were created 4 custom hooks all very similar to each other.
Those use similar queries and mutations. The scope of all 4 is to manage an API called StudyConfiguration. I need to make it merged in one custom hook and in an efficient way.
The 4 custom hooks are called
useConfiguration,
useSetConfiguration,
useStudyConfigurationOverride,
useUnSetConfiguration
I tried to emerge as follows and below it, you will see the 4 custom hooks along with details of their usage. The goal is to have all 4 in one in the right way.
This is the new custom hook I did by merging the 4 in one but I'm not convinced that could be the right way
import { gql, useMutation, useQuery } from '#apollo/client';
import { useCallback } from 'react';
const GET = gql`
query WEB_useConfiguration($name: String!, $scope: StudyConfigurationScope) {
studyConfiguration(name: $name, filter: $scope) {
name
value
configurationOverrideChain {
value
scope
}
}
}
`;
const GET_OVERRIDE = gql`
query WEB_useConfigurationOverride($name: String!, $scope: StudyConfigurationScope) {
studyConfiguration(name: $name, filter: $scope) {
configurationOverrideChain {
value
scope
}
}
}
`;
const SET = gql`
mutation WEB_setConfiguration($input: setStudyConfiguration!) {
setStudyConfiguration(input: $input) {
name
value
}
}
`;
const UNSET = gql`
mutation WEB_unSetConfiguration($input: unsetStudyConfiguration!) {
unsetStudyConfiguration(input: $input) {
name
value
}
}
`;
const useConfiguration = ({ name, scope, defaultValue = null }) => {
const { data } = useQuery(GET, {
variables: { name, scope },
fetchPolicy: 'network-only',
});
const value = data?.studyConfiguration?.value;
if (!value) {
return defaultValue;
}
try {
return JSON.parse(value);
} catch {
return value;
}
};
const useStudyConfigurationOverride = ({ name, scope }) => {
const { data } = useQuery(GET_OVERRIDE, {
variables: { name, scope },
fetchPolicy: 'network-only',
});
const value = data?.studyConfiguration?.configurationOverrideChain;
if(!value) return []
return value;
};
const useSetConfiguration = input => {
const [setStudyConfiguration] = useMutation(SET);
const executeSetConfiguration = useCallback(() => {
return setStudyConfiguration({ variables: { input: { ...input } } });
}, [input, setStudyConfiguration]);
return [executeSetConfiguration];
};
const useUnSetConfiguration = input => {
const [unSetStudyConfiguration] = useMutation(UNSET);
const executeUnSetConfiguration = useCallback(() => {
return unSetStudyConfiguration({ variables: { input: { ...input } } });
}, [input, unSetStudyConfiguration]);
return [executeUnSetConfiguration];
};
export { useConfiguration, useSetConfiguration, useUnSetConfiguration, useStudyConfigurationOverride };
Now the single hooks with an example of how they are used
useConfiguration
This custom hook is for getting the Study configuration from the API and returns a value. The reason we have a try/catch is that depending on what config is requested the value can be a string or a boolean. So if the parse fails we return the value directly.
import { gql, useQuery } from '#apollo/client';
const QUERY = gql`
query WEB_useConfiguration($name: String!, $scope: StudyConfigurationScope) {
studyConfiguration(name: $name, filter: $scope) {
name
value
}
}
`;
export const useConfiguration = ({ name, scope, defaultValue = null }) => {
const { data } = useQuery(QUERY, {
variables: { name, scope },
fetchPolicy: 'network-only',
});
const value = data?.studyConfiguration?.value;
if (!value) {
return defaultValue;
}
try {
return JSON.parse(value);
} catch {
return value;
}
};
The above is used as an example
const emailSender = useConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
In this case, we get back a string like value = email#email.com
useStudyConfigurationOverride
This custom hook is actually almost the same as the above one but we getting the second part of the result of the same query. It is giving back configurationOverrideChain which is an array of tracked changes for the configuration
import { gql, useQuery } from '#apollo/client';
const QUERY = gql`
query WEB_useConfigurationOverride($name: String!, $scope: StudyConfigurationScope) {
studyConfiguration(name: $name, filter: $scope) {
name
configurationOverrideChain {
value
scope
}
}
}
`;
export const useStudyConfigurationOverride = ({ name, scope }) => {
const { data } = useQuery(QUERY, {
variables: { name, scope },
fetchPolicy: 'network-only',
});
const value = data?.studyConfiguration?.configurationOverrideChain;
if(!value) return []
return value;
};
An example of usage
const studyOverrides = useStudyConfigurationOverride({
name: 'messaging.email.sender.address',
scope: { studyId },
});
results in an array of the overrides as
[
{
"value": "global#test.com",
"scope": "GLOBAL"
},
{
"value": "noreply#test.com",
"scope": "STUDY"
},
{
"value": "hello#app.trialbee.com",
"scope": "DEFAULT"
}
]
useSetConfiguration
This custom hooks set a new configuration
import { gql, useMutation } from '#apollo/client';
import { useCallback } from 'react';
const SET_STUDY_CONFIGURATION = gql`
mutation WEB_setConfiguration($input: setStudyConfiguration!) {
setStudyConfiguration(input: $input) {
name
value
}
}
`;
export const useSetConfiguration = input => {
const [setStudyConfiguration] = useMutation(SET_STUDY_CONFIGURATION);
const executeSetConfiguration = useCallback(() => {
return setStudyConfiguration({ variables: { input: { ...input } } });
}, [input, setStudyConfiguration]);
return [executeSetConfiguration];
};
Example of usage
const [setNoReplyStudyEmail] = useSetConfiguration({
name: 'messaging.email.sender.address',
value: noReplyEmail,
scope: { studyId },
});
useUnsetConfiguration
This custom hook is for remove the configuration setted like from above mutation
import { gql, useMutation } from '#apollo/client';
import { useCallback } from 'react';
const UNSET_STUDY_CONFIGURATION = gql`
mutation WEB_unSetConfiguration($input: unsetStudyConfiguration!) {
unsetStudyConfiguration(input: $input) {
name
value
}
}
`;
export const useUnSetConfiguration = input => {
const [unSetStudyConfiguration] = useMutation(UNSET_STUDY_CONFIGURATION);
const executeUnSetConfiguration = useCallback(() => {
return unSetStudyConfiguration({ variables: { input: { ...input } } });
}, [input, unSetStudyConfiguration]);
return [executeUnSetConfiguration];
};
Example
const [setSmsMessagingDefault] = useUnSetConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
});
As extra details this is a component using this implementation
import {
useConfiguration,
useSetConfiguration,
useStudyConfigurationOverride,
useUnSetConfiguration
} from '#lib/hooks/useConfiguration';
import { FormControlLabel, FormGroup, Switch, Typography } from '#mui/material';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
const StudyConfiguration = ({ studyId }) => {
const intl = useIntl();
// !TODO: make one smart hook/s
// Tracking the study scope/value
const studyOverrides = useStudyConfigurationOverride({
name: 'messaging.email.sender.address',
scope: { studyId },
});
console.log('studyOverrides: ', studyOverrides);
// Getting the study config
const smsEnabled = useConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
defaultValue: false,
});
console.log('smsEnabled: ', smsEnabled);
const emailSender = useConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
console.log('emailSender: ', emailSender);
const [studyConfOverride, setStudyConfOverride] = useState(studyOverrides);
const [valueEmailReply, setValueEmailReply] = useState(emailSender);
const [valueSmsConf, setValueSmsConf] = useState(smsEnabled);
useEffect(() => {
if (studyConfOverride.length !== studyOverrides.length) {
setStudyConfOverride(studyOverrides);
}
}, [studyOverrides]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (valueEmailReply !== emailSender) {
setValueEmailReply(emailSender);
}
}, [emailSender]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (valueSmsConf !== smsEnabled) {
setValueSmsConf(smsEnabled);
}
}, [smsEnabled]); // eslint-disable-line react-hooks/exhaustive-deps
// Building the default reply email based on 'SCOPE'
// !TODO: study overrides sort in study service (TBD)
let defaultEmail;
if (studyOverrides.find(o => o.scope === 'GLOBAL')) {
const { value } = studyOverrides.find(o => o.scope === 'GLOBAL');
defaultEmail = value;
} else if (studyOverrides.find(o => o.scope === 'DEFAULT')) {
const { value } = studyOverrides.find(o => o.scope === 'DEFAULT');
defaultEmail = value;
}
// Extracting the email domain from default email and used to make a 'noreply#domain.xxx'
const emailDomain = defaultEmail?.substring(defaultEmail.indexOf('#'));
const noReplyEmail = `noreply${emailDomain}`;
// Set study config
const [setNoReplyStudyEmail] = useSetConfiguration({
name: 'messaging.email.sender.address',
value: noReplyEmail,
scope: { studyId },
});
const [setSmsMessagingDisable] = useSetConfiguration({
name: 'messaging.recruitment.sms.enable',
value: 'false',
scope: { studyId },
});
// unSet study config
const [setDefaultStudyEmail] = useUnSetConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
const [setSmsMessagingDefault] = useUnSetConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
});
const handleReplyEmailChange = async event => {
setValueEmailReply(event.target.checked ? defaultEmail : noReplyEmail);
event.target.checked
? await setDefaultStudyEmail()
: await setNoReplyStudyEmail();
};
const handleSmsConf = async event => {
setValueSmsConf(event.target.checked);
event.target.checked
? await setSmsMessagingDefault()
: await setSmsMessagingDisable();
};
const isEmailEnabled = valueEmailReply === defaultEmail;
return (
<FormGroup>
<FormControlLabel
control={
<Switch
data-testid="email-reply"
checked={isEmailEnabled}
onChange={handleReplyEmailChange}
/>
}
label={
<Typography color="textPrimary">
{intl.formatMessage(
{
defaultMessage:
'Allow candidates to reply to emails (send from {replyEmailTxt} instead of {noReplyTxt})',
},
{ replyEmailTxt: defaultEmail, noReplyTxt: noReplyEmail },
)}
</Typography>
}
/>
<FormControlLabel
control={
<Switch
data-testid="sms-enable"
checked={valueSmsConf}
onChange={handleSmsConf}
/>
}
label={
<Typography color="textPrimary">
{intl.formatMessage({
defaultMessage: `SMS messaging`,
})}
</Typography>
}
/>
</FormGroup>
);
};
export default StudyConfiguration;

Answering my own question in case of someone will have similar issues.
After hours I found the following solution merging all the custom hooks into one hook.
The solution
import { gql, useMutation, useQuery } from '#apollo/client';
import { useCallback } from 'react';
const GET = gql`
query WEB_useConfiguration($name: String!, $scope: StudyConfigurationScope) {
studyConfiguration(name: $name, filter: $scope) {
name
value
configurationOverrideChain {
value
scope
}
}
}
`;
const SET = gql`
mutation WEB_setConfiguration($input: setStudyConfiguration!) {
setStudyConfiguration(input: $input) {
name
value
}
}
`;
const UNSET = gql`
mutation WEB_unsetConfiguration($input: unsetStudyConfiguration!) {
unsetStudyConfiguration(input: $input) {
name
value
}
}
`;
const useGetConfiguration = ({ name, scope, defaultValue }) => {
const { data } = useQuery(GET, {
variables: { name, scope },
fetchPolicy: 'network-only',
});
const value = data?.studyConfiguration?.value;
const overrideChain = data?.studyConfiguration?.configurationOverrideChain;
const studySettings = {};
if (!value) studySettings.value = defaultValue;
else {
try {
studySettings.value = JSON.parse(value);
} catch {
studySettings.value = value;
}
}
if (!overrideChain) studySettings.overrideChain = [];
else {
studySettings.overrideChain = overrideChain;
}
return studySettings;
};
const useSetConfiguration = ({ name, scope }) => {
const [setStudyConfiguration] = useMutation(SET);
const executeSetConfiguration = useCallback(
(value) =>
setStudyConfiguration({ variables: { input: { name, scope, value } } }),
[name, scope, setStudyConfiguration],
);
return executeSetConfiguration;
};
const useUnsetConfiguration = input => {
const [unsetStudyConfiguration] = useMutation(UNSET);
const executeUnsetConfiguration = useCallback(
() => unsetStudyConfiguration({ variables: { input: { ...input } } }),
[input, unsetStudyConfiguration],
);
return executeUnsetConfiguration;
};
export const useConfiguration = ({ name, scope, defaultValue = null }) => {
// GET
const studySettings = useGetConfiguration({ name, scope, defaultValue });
// SET
const setStudyConfiguration = useSetConfiguration({ name, scope });
// UNSET
const unsetStudyConfiguration = useUnsetConfiguration({ name, scope });
return [studySettings, setStudyConfiguration, unsetStudyConfiguration];
};
and an example of how it is used in a possible component
const [emailSettings, setSenderEmail, unsetSenderEmail] = useConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});

Related

Uncaught TypeError: variable is not iterable

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.

Apollo MockedProvider not returning expected data

I wrote a hook that calls apollo useQuery. It's pretty simple:
useDecider:
import { useState } from 'react';
import { useQuery, gql } from '#apollo/client';
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = name => {
const [enabled, setEnabled] = useState(false);
useQuery(GET_DECIDER, {
variables: {
name
},
onCompleted: data => {
const decision = data?.deciders[0]?.decision;
setEnabled(decision);
},
onError: error => {
return error;
}
});
return {
enabled
};
};
I'm trying to test it now and the MockedProvider is not returning the expected data:
import React from 'react';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom';
import { MockedProvider } from '#apollo/client/testing';
import { useDecider, GET_DECIDER } from './useDecider';
const getMock = (value = false, decider = '') => [
{
request: {
query: GET_DECIDER,
variables: {
name: decider
}
},
result: () => {
console.log('APOLLO RESULT');
return {
data: {
deciders: [
{
decision: value,
name: decider,
value: 10
}
]
}
};
}
}
];
const FakeComponent = ({ decider }) => {
const { enabled } = useDecider(decider);
return <div>{enabled ? 'isEnabled' : 'isDisabled'}</div>;
};
const WrappedComponent = ({ decider, value }) => (
<MockedProvider mocks={getMock(value, decider)} addTypename={false}>
<FakeComponent decider={decider} />
</MockedProvider>
);
describe('useDecider', () => {
it('when decider returns true', () => {
// should return true
render(<WrappedComponent decider="fake_decider" value={true} />);
screen.debug();
const result = screen.getByText('isEnabled');
expect(result).toBeInTheDocument();
});
});
I simplified your hook implementation and put together a working example:
import { useQuery, gql } from "#apollo/client";
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = (name) => {
const { data } = useQuery(GET_DECIDER, { variables: { name } });
return { enabled: data?.deciders[0]?.decision || false };
};
Note that in the test I also updated your getBy to an await findBy:
describe("useDecider", () => {
it("when decider returns true", async () => {
// should return true
render(<WrappedComponent decider="fake_decider" value={true} />);
screen.debug();
const result = await screen.findByText("isEnabled");
expect(result).toBeInTheDocument();
});
});
This is because you need to wait for your API call to complete before the data will be on the page, hence you would not expect the data to be there on the first render.
From https://www.apollographql.com/docs/react/development-testing/testing/#testing-the-success-state
To test how your component is rendered after its query completes, you
can await a zero-millisecond timeout before performing your checks.
This delays the checks until the next "tick" of the event loop, which
gives MockedProvider an opportunity to populate the mocked result
try adding before your expect call
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});

Rendering multiple times?

I have a form component, and the reference of input fields are linked to the useForm reducer with references. I have to set a initial form state after setting the input field references? I have done as below. But it is rendering thrice. How to solve this rendering issue?
import React, { useState } from 'react';
const useForm = () => {
const [ formState, setFormState ] = useState({});
const refs = useRef({});
const register = useCallback(( fieldArgs ) => ref => {
if(fieldArgs) {
const { name, validations, initialValue } = fieldArgs;
refs.current[name] = ref;
}
console.log('Register rendered');
}, []);
useEffect(() => {
console.log('Effect Rendered');
const refsKeys = Object.keys(refs.current);
refsKeys.forEach(refKey => {
if(!formState[refKey]) {
setFormState(prevState => {
return {
...prevState,
[refKey]: {
value: '',
touched: false,
untouched: true,
pristine: true,
dirty: false
}
}
});
}
});
}, [ refs ]);
return [ register ];
}
export { useForm };
And the app component as below
const App = () => {
const [ register ] = useFormsio();
return(
<form>
<input
type = 'email'
placeholder = 'Enter your email'
name = 'userEmail'
ref = { register({ name: 'userEmail' }) } />
<button
type = 'submit'>
Submit
</button>
</form>
)
}
How to solve this multiple rendering issue?
I think the issue in the code above is whenever refs changes you need to loop through all the fields in form and set the state.
Why don't you set the state in register method?
const register = useCallback(( fieldArgs ) => ref => {
if(fieldArgs) {
const { name, validations, initialValue } = fieldArgs;
if(!refs.current[name] ) {
refs.current[name] = ref;
setFormState(prevState => {
return {
...prevState,
[refKey]: {
value: '',
touched: false,
untouched: true,
pristine: true,
dirty: false
}
}
});
}
}
console.log('Register rendered');
}, []);

Save search term on refresh React

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

React + TS: How to call a method from outside of a React Functional Component

Im wondering how I can call a method from outside of a React Functional Component. I wrote the function GetUsedLockers() which gets all the used lockers and returns amount. Now I want to call this function from another another component (OrgLocker.tsx) and display the data from the getUsedLockers() function there.
OrgLockerTables.tsx
const OrgLockerTables: React.FC = () => {
const lockerCall = 'lockers';
const [lockerData, setLockerData] = useState({
id: 0,
guid: "",
is_currently_claimable: false
}[""]);
useEffect(() => {
componentConsole().then((res) => {
setLockerData(res);
})
// eslint-disable-next-line
}, []);
if (!lockerData) return (<div>Loading...</div>);
//function to get all used lockers
function getUsedLockers() {
let amount = 0;
for (let i = 0; i < lockerData.length; i++) {
if (!lockerData.is_currently_claimable) {
amount++;
}
}
console.log('log from getusedlockers, amount: ', amount)
return (amount)
}
// function to get JSON data from the API
function componentConsole(): Promise<any> {
return new Promise<any>((resolve, reject) => {
http.getRequest('/' + lockerCall).then((res) => {
let data = res.data.data;
console.log('data:', data);
resolve(res.data.data);
}).catch((error) => {
console.log(error);
reject();
});
})
}
}
OrgLocker.tsx
import OrgLockerTables from '../tables/orgLockerTables';
const OrgLockers: React.FC = () => {
let lockerTable = new OrgLockerTables();
return (
<div className="main-div-org">
<p>Used</p>
<p>{lockerTable.getUsedLockers()}</p>
</div>
);
}
export default OrgLockers;
When trying to make a call to OrgLockerTables and storing it in the lockerTable let it gives the following error:
Expected 1-2 arguments, but got 0.ts(2554)
Any help would be greatly appreciated!
I've restructured everything making it more understandable, I hope you don't mind according to what I think you want the comment above.
locker-model.ts - The type for the particular data being called back is found
export type Locker = {
id: number;
guid: string;
isCurrentlyClaimable: boolean;
}
locker-business.ts - Where all the business logic is carried out, from the call for data to the calculation based on it
import { Locker } from "./locker-models";
const lockerCall = 'lockers';
const mockedData: Locker[] = [{
id: 0,
guid: "sample",
isCurrentlyClaimable: false,
},
{
id: 1,
guid: "sample2",
isCurrentlyClaimable: true,
},
{
id: 2,
guid: "sample3",
isCurrentlyClaimable: true,
}]
// Mocked function from your backend (componentConsole where you use lockerCall variable)
export const getLockersData = (): Promise<Locker[]> => Promise.resolve(mockedData);
export const getAmount = (lockers: Locker[]): number => {
let amount = 0;
!!lockers ?
lockers.filter(({isCurrentlyClaimable}) => { if(isCurrentlyClaimable) amount++ })
: 0;
return amount;
};
index.tsx - Here are both components that make the call to get the data and render the result you're looking for
import React, { Component } from 'react';
import { Locker } from './locker-models';
import { getLockersData, getAmount } from './locker-business';
import './style.css';
type OrgLockersProps = {
amount: number;
}
const OrgLockers: React.FC<OrgLockersProps> = ({ amount }) => {
return (
<div className="main-div-org">
<p>Lockers used:</p>
<p>{amount}</p>
</div>
);
}
type OrgLockerTableProps = {};
const OrgLockerTable : React.FC<OrgLockerTableProps> = props => {
const [lockerData, setLockerData] = React.useState<Locker[]>([]);
React.useEffect(() => {
getLockersData().then(response => setLockerData(response));
}, []);
const amount = getAmount(lockerData);
return (
<div>
<OrgLockers amount={amount} />
</div>
);
};
You can see the example here
You can create new .js file like Helpers.js and define export function with parameter it like that
export function getUsedLockers(lockerData) {
let amount = 0;
//Check your loop it can be like that
for (let i = 0; i < lockerData.length; i++) {
if (!lockerData[i].is_currently_claimable) {
amount++;
}
}
console.log('log from getusedlockers, amount: ', amount)
return (amount)
}
Then import it where do you want to use.
import {getUsedLockers} from "../Helpers";
And use it like that:
const amount = getUsedLockers(data);

Categories