How to fix invalid hook call error in my airtable extension? - reactjs

I'm getting this error in my Airtable blocks extension:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I'm unsure as to why. My program uses airtable blocks api to create a dropdown that sets a useState value, before useEffect updates a database live. Both hooks are being used inside the function body, and the function should be a react component, so I don't understand where the error lies. I've seen it can be due to react version conflicts and such as well, but I'm not sure how to confirm whether or not that is the underlying issue.
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import {
Select,
initializeBlock,
SelectSynced,
useBase,
useRecords,
BaseProvider,
useGlobalConfig,
expandRecord,
TablePickerSynced,
ViewPickerSynced,
FieldPickerSynced,
FormField,
Input,
Button,
Box,
Icon,
} from '#airtable/blocks/ui';
import { FieldType } from '#airtable/blocks/models';
const base = useBase();
const table = base.getTable("National Works In Progress");
export default function FilterApp() {
// YOUR CODE GOES HERE
let records = useRecords(table);
var aunumbers_array = [];
const [value, setValue] = useState("");
const queryResult = table.selectRecords({ fields: ["AU Number"] });
records.forEach(function (x) {
if (aunumbers_array.indexOf(x.getCellValueAsString("AU Number"), -1)) {
aunumbers_array.push({ value: x.getCellValueAsString("AU Number"), label: x.getCellValueAsString("AU Number") })
}
});
queryResult.unloadData();
let updates = [];
useEffect(() => {
const fetchData = async () => {
records.forEach(function (x) {
if (x.getCellValueAsString('AU Number') == value) {
updates.push({ id: x.id, fields: { 'Matches Filter': true } });
console.log(value);
}
else if (x.getCellValueAsString('AU Number') !== value && x.getCellValueAsString('Matches Filter') == 'checked') {
updates.push({ id: x.id, fields: { 'Matches Filter': false } });
}
});
while (updates.length) {
await table.updateRecordsAsync(updates.splice(0, 50));
}
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);
}, [value])
return (
<div>
<FormField label="Text field">
<Select
options={aunumbers_array}
value={value}
onChange={newValue => setValue(newValue.toString())}
width="320px"
/>
</FormField>
</div>
);
}

The error is because you are calling useBase, a custom hook in an invalid way.
const base = useBase();
This is wrong way of calling a hook as hooks can only be called inside of the body of a function component.
Move the hook call inside FilterApp() functional component.

Related

Why getting value from local storage breaks my theme toggle?

I have this piece of code in React. I want to push a fragment of my state to local storage to keep a chosen theme on page refresh. Toggle itself works fine until I want to get the value from local storage:
import React, { useEffect, useState } from 'react';
import './ThemeToggle.css'
export function ThemeToggle() {
const [ isDark, setIsDark ] = useState(true);
const root = document.querySelector(':root')
const storeUserSetPreference = (pref) => {
localStorage.setItem("isDark", pref);
};
const getUserSetPreference = () => {
return localStorage.getItem("isDark");
};
const changeThemeHandler = () => {
setIsDark(!isDark)
storeUserSetPreference(isDark)
}
useEffect(() => {
let userPref = getUserSetPreference()
console.log(userPref)
if(!isDark){
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}, [isDark, root.classList ])
return (
<div className='toggle'>
<input type='checkbox' id='darkmode-toggle' onChange={changeThemeHandler} defaultChecked={!isDark} />
<label for='darkmode-toggle' id='toggle-label' />
</div>
)
}
But as soon as I try to get value from local storage in useEffect like this:
useEffect(() => {
let userPref = getUserSetPreference()
console.log(userPref)
if(!iuserPref){
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}, [isDark, root.classList ])
then it doesn't work anymore. I get the proper value logged to my console.
I also tried adding userPref as dependency but then I get error that userPref is not defined. Any ideas?
Here is a sandbox version where I was able to replicate this issue:
Live version on codesandbox
Local storage only save strings and you are saving "false" or "true" in your local storage not false and true itself. they are string not boolean but in your code you used it as a boolean.
if you change your useEffect to this it will solve the error:
useEffect(() => {
let userPref = getUserSetPreference()
if(iuserPref == "false"){
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}, [isDark, root.classList])
The issue with LocalStorage is that it only works with strings. When you try to store a boolean value, it gets automatically converted to a string. Therefore, if you use localStorage.getItem('isDark'), it will return either the string 'true' or 'false', but both of these strings will be considered truthy values in JavaScript. So you need to explicitly serialize values
If I understand your code correctly, you can simplify it significantly:
import React, { useEffect, useState } from "react";
import "./ThemeToggle.css";
export function ThemeToggle() {
const [isDark, setIsDark] = useState(
JSON.parse(localStorage.getItem("isDark"))
);
console.log("isDark", isDark);
useEffect(() => {
const rootElement = document.querySelector(":root");
localStorage.setItem("isDark", JSON.stringify(isDark));
if (isDark) {
rootElement.classList.add("dark");
} else {
rootElement.classList.remove("dark");
}
}, [isDark]);
return (
<div className="toggle">
<input
type="checkbox"
id="darkmode-toggle"
onChange={(e) => setIsDark(e.target.checked)}
checked={isDark}
/>
<label for="darkmode-toggle" id="toggle-label" />
</div>
);
}
Sandbox

Apollo Client: Can't put ReactHook after Conditional, but React Hook needs data available only after conditional?

Order of Operations issues it seems.
Can't use a React Hook after a Conditional as I get this error:
Error: Rendered more hooks than during previous render
This React Hook is 3rd party and can't change it. This React Hook needs data that is only available after the conditional...so I get another error of data not defined
Ok, how about declare a new variable (empty array) to put in 3rd party React hook before conditional? Then after conditional reassign with data that shows up. Doesn't work either... as I get error that it is using the same key...well because the 3rd party hook is getting an empty array variable first. What do I do?
import { libraryHook } from '3rd party'
import { useQuery } from '#apollo/client'
const myComponent = () => {
const {loading, error, data } = useQuery(MY_QUERY, { variables: {someVariable: fromDatabase}
const { 3rdpartyVariable } = libraryHook(data);
// Hook needs to be above conditional so I don't get error,
//but data only available after conditional.
if (loading) return <div>Loading...</div>;
if (error) return <div>{error.message}</div>;
console.log(data);
return(
<div>
{data}
</div>
)
}
export default myComponent;```
There is another hook called useLazyQuery that can be used in this case, it provides you a function that can be called to execute your query. This is how you can do it
import { gql, useLazyQuery } from "#apollo/client";
const GET_GREETING = gql`
query GetGreeting($language: String!) {
greeting(language: $language) {
message
}
}
`;
function Hello() {
const [loadGreeting, { called, loading, data }] = useLazyQuery(
GET_GREETING,
{ variables: { language: "english" } }
);
if (called && loading) return <p>Loading ...</p>
if (!called) {
return <button onClick={() => loadGreeting()}>Load greeting</button>
}
return <h1>Hello {data.greeting.message}!</h1>;
}
Refer Docs

How to reset/clear React Context with a button

I can't clear context even when I'm setting the state to null in the context file. Can anyone tell me what's wrong with my code? This is my code:
import React, { createContext, useState } from 'react';
export const MembersContext = createContext([{}, () => {}]);
export const MembersProvider = ({ children }) => {
const [members, setMembers] = useState(null);
const refreshMembers = async () => {
try {
const data = await request('api/members');
setMembers(data);
} catch (error) {
console.log('ERROR: ', error);
}
};
const clearMembers = () => {
setMembers(null);
console.log('CLEARED MEMBERS IN CONTEXT FILE', members); // not cleared
};
return (
<MembersContext.Provider
value={{
members,
clearMembers,
}}
>
{children}
</MembersContext.Provider>
);
};
Then in my sign out page I have a button to use the clear context function:
import React, { useContext } from 'react';
import {
Button,
} from 'react-native';
import { MembersContext } from '../MembersContext';
const Settings = () => {
const { clearMembers, members } = useContext(MembersContext);
const clearContext = () => {
clearMembers();
console.log('CLEARED MEMBERS?: ', members); // not cleared
//logout()
};
return (
<Button onPress={()=> clearContext()}>Log Out</Button>
);
};
export default Settings;
My console log and screen still shows the data from the previous session.
Let's see both scenarios :-
console.log('CLEARED MEMBERS?: ', members); // not cleared - Here you're not logging the value of members that will get updated but the value of members on which clearContext() closed over i.e. the current state value (before update).
console.log('CLEARED MEMBERS IN CONTEXT FILE', members); // not cleared - This isn't the right way to see if members changed. The state
update is async. Doing a console.log(...) just after updating
members won't work. And I think the reason is same as above. It won't work because the clearMembers() function closes over current value of members.
Each update to members also result's in a new clearMembers()/clearContext() (due to re-render), atleast in this case. That's why these functions can always access the latest state.
To check whether members actually updated, log the value either in the function body of Settings or inside useEffect with members in it's dependency array.

React Hooks, copied data keeps changing original source data

I'm creating a filtering page. I make a call to the api and then I copy the data so that with each filter requests i'm filtering from the original source. But for some reason when i'm filtering it's modifying my source. So when i filter twice its giving me wrong data. I put flags to make sure my reducer isnt' being called again.
import React, {
useEffect,
useState
} from 'react';
import {
useDispatch,
useSelector
} from "react-redux";
import {
useParams
} from "#reach/router"
import {
fetchSession
} from "../actions/dataActions";
import _ from 'lodash'
import {
useFormik
} from 'formik';
function PageFilter() {
const sessionData = useSelector(state => state.sessionData); //redux
const [filteredData, setFilteredData] = useState([]);
const formik = useFormik({
initialValues: {
date: '',
sessionType: '',
events: ''
},
onSubmit: values => {
filterData(values) //when i hit submit filter data
},
});
function filterData(values) {
let tempFilter = [];
if (values.date != '') {
sessionData.schedule_by_day.map((c, i) => {
if (c.date === values.date) {
tempFilter = [...tempFilter, c];
}
});
} else {
tempFilter = [...sessionData.schedule_by_day];
}
if (values.sessionType != '') {
tempFilter.map((c, i) => {
let tempSessionType = [];
c.session_types.map(s => {
if (parseInt(values.sessionType) == s.id) {
tempSessionType = [...tempSessionType, s]
}
});
tempFilter[i].session_types = [...tempSessionType];
//!!!!!!!
//this is a problem issue, it keeps changing my original data
//so when i call this again it's filtering from a modified SessionData
//!!!!!!!
})
} else {
}
setFilteredData([...tempFilter])
}
useEffect(() => {
dispatch(fetchSession(params.id)); //request data and updates state.sessionData
}, [])
useEffect(() => {
///when sessionData is updated filter the Data based off default form values which displays everything
if (_.isEmpty(sessionData) == false) {
filterData(formik.values)
}
}, [sessionData])
Yes, you are doing a state mutation with tempFilter[i].session_types = [...tempSessionType];. When you update any part of state you must copy it first then update the property.
It is actually rather unclear what you are really trying to do in this handler. You map a bunch of data, but don't return or save the result. It appears as though you meant to use array::forEach to issue a side-effect of pushing elements into an external array. That being said, the following is how you would/should copy the element at the specified index and create a new session_types property object reference so you are not mutating the original reference.
tempFilter[i] = {
...tempFilter[i],
session_types: [...tempSessionType],
}

React useState - setValue rerenders the component even though the value is the same

I'm using useState hook in a custom hook.
I'm calling the setValue function that is returned from the useState twice:
1) After an onChange event
2) After the component was notified of a change from the server.
The flow of events is:
onChange event (in the component) triggers setValue = rerender
I'm using a useEffect (=after rerender) hook to update the server of the change - API function is called to update the server
I have a custom service which receives the server's response and notifies the component
When notified the component calls setValue again but in this case, the value is the same so no need to rerender.
My problem is the component gets rerendered after it was notified of the changes even though the value received is the same.
My code:
Gain Component
import * as React from 'react';
import { useDSPParamUpdate } from '../../../Hooks/useDSPParamUpdate';
import { ControlParamProps } from '../..';
const Gain = (props: ControlParamProps) => {
let min: number = 0;
let max: number = 0;
const { paramId, controlId } = props;
const { param, value, setValue } = useDSPParamUpdate({ controlId, paramId })
if (param && param.range && param.range.length >= 2) {
min = param.range[0];
max = param.range[1];
}
/*calls the setValue from the hook*/
const handleChange = (event: any) => {
const newValue = event.target.value;
setValue(newValue);
}
return (
<div className="gain">
{max}
<input className="dsp-action range-vertical" type="range"
min={min}
max={max}
value={value}
onChange={handleChange} />
{min}
</div>
);
}
export default Gain;
useDSPParamUpdate - custom hook
import * as React from 'react';
import { ControlParamProps } from '../dsp';
import { dspService } from '../../core/services/dsp.service';
export function useDSPParamUpdate(props: ControlParamProps) {
const initValue = ...
const [value, setValue] = React.useState(initValue);
function updateDevice() {
// calls some API func to update the server (sends the value)
}
// subscribes to server changes
React.useEffect(() => {
// subscribrs to server notifications
let unsubscribe = dspService.subscribe((newVal) => setValue(newVal));
return () => {
unsubscribe();
};
}, []);
// sends data to the server after update
React.useEffect(() => {
updateDevice();
}, [value]);
return { param, value, setValue };
}
Typically it's not an issue if render() is called extra time.
But if you want you may guard calling setValue() by checking if value is the same
let unsubscribe = dspService.subscribe(
(newVal) =>
newVal !== value && setValue(newVal)
);
Maybe slightly verboose way but it's the same approach as typically used in componentDidUpdate
Note that useState does not provide any logic like shouldComponentUpdate has. So if you want to make it in more declarative way you have to refactor your component to be class-based accessor of PureComponent.

Resources