some trouble with state in TypeScript React.
A Child component passes a ’terminal’ object to the Parent through a passed function named returnTerminal(). This terminal object is then stored as a useState _object. useEffect() says that _object is not null eventually, but callback() continuously maintains that _object is null, and I’m not sure why.
Parent Component
const Parent: React.FunctionComponent<ParentProps> = () => {
const [_object, set_object] = useState<Terminal>(null);
const handleGetTerminal = (terminal: Terminal) => {
set_object(terminal);
};
useEffect(() => {
if (_object !== null) {
_object.writeln("_object is not null"); // This prints just fine
}
}, [_object]);
return (
<Child
returnTerminal={(term) => handleGetTerminal(term)}
callback={() => {
console.log(_object === null); // This returns true - the object is null for some reason???
}}
/>
);
};
Child Component
const Child: React.FunctionComponent<ChildProps> = (props) => {
const { callback, returnTerminal } = props;
// The ref where the terminal will be stored and returned
const ref = useRef<HTMLDivElement>(null);
// The xterm.js terminal object
const terminal = new Terminal();
useLayoutEffect(() => {
// Calls whenever terminal is typed into
if (callback) terminal.onData(callback);
// Mount terminal to the DOM
if (ref.current) terminal.open(ref.current);
// Pass the terminal to the parent object
returnTerminal(terminal);
}, []);
return <div ref={ref}></div>;
};
export default Child;
The callback() always returns that _object is null, no matter how long I wait.
The answer ended up being the use of useImperativeHandle and forwardRef. Here’s the completed code.
Parent
import Child from "./child";
interface ParentProps {
name?: string;
}
const Parent: React.FunctionComponent<ParentProps> = () => {
type ChildHandle = ElementRef<typeof Child>;
const childRef = useRef<Child>(null);
let _object: Terminal; // same Terminal type that Child uses
useEffect(() => {
if (childRef.current) {
_object = childRef.current.get(); // imperitive handle function
_object.writeln(“_object loaded”);
}
}, []);
return (
<Child
ref={childRef}
callback={() => {
terminal.writeln("_object is not null”); // THIS finally works
}}
/>
);
};
Child
interface ChildProps {
callback?(data: string): void;
}
type ChildHandle = {
get: () => Terminal;
};
const Child: ForwardRefRenderFunction<ChildHandle, ChildProps> = (
props,
forRef
) => {
const { callback } = props;
const terminal = new Terminal();
// The ref where the terminal will be stored and returned
const ref = useRef<HTMLDivElement>(null);
useImperativeHandle(forRef, () => ({
get() {
return terminal;
},
}));
useLayoutEffect(() => {
// Calls whenever terminal is typed into
if (callback) terminal.onData(callback);
// Mount terminal to the DOM
if (ref.current) terminal.open(ref.current);
}, []);
return <div ref={ref}></div>;
};
export default forwardRef(Child);
Related
I am learning react and trying set object from queryStr for later reuse,
can't set searchFilter in useEffect
this line prinst null:
console.log(searchFilter.transactionId)//prints null
interface TransactionSearchFilter {
transactionId?: number;
}
const TransactionList: any = (props: any) => {
const queryStr = location.search.substring(1);
const [searchFilter, setSearchFilter] = React.useState<TransactionSearchFilter>({
transactionId: null,
});
const parseQueryParams = () => {
const queryObj = QueryString.parse(queryStr);
console.log(queryObj.transactionId)//prints 10
setSearchFilter({ ...searchFilter, ...queryObj });
console.log(searchFilter.transactionId)//prints null
};
React.useEffect(() => {
parseQueryParams();
}, []);
return (<div>Hello World</div>);
}; export default TransactionList;
I have an issue on any app refresh, ONE of my Contexts is returning undefined functions.
I have the following structure:
export default function AppWrapper() {
return (
<GlobalProvider>
<ChildProvider>
<App />
</ChildProvider>
</GlobalProvider>
);
}
I am getting undefined on functions from ChildProvider, which looks like this:
const INITIAL_STATE = {
orders: [],
error: null,
};
const ChildContext = React.createContext({ ...INITIAL_STATE });
const ChildConsumer = ChildContext.Consumer;
const OrderProvider = (props) => {
const { children } = props;
const { store, userId } = useContext(GlobalContext);
const [state, setState] = React.useReducer((state, newState) => {
const combinedState = { ...state, ...newState };
return combinedState;
}, INITIAL_STATE);
const getDataFromStore = async () => {
if (store?.id) {
try {
const { error, data } = await getOrdersByStore({store.id});
if (data) {
const data1 = <iterating through data>;
await setState({ orders: data1 });
} else {
await setState({ error: error });
}
} catch (e) {
await setState({ error: e });
}
}
};
const actions = { getDataFromStore};
return <ChildContext.Provider value={{ ...state, ...actions }}>{children}</ChildContext.Provider>;
};
export { ChildContext, ChildConsumer, ChildProvider };
And in Home.js, I call these functions as such:
const { getDataFromStore } = useContext(ChildContext)
useEffect(() => {
const getOrders = async () => {
store?.name && (await getDataFromStore());
};
getOrders();
}, [store]);
And in Home.js is where that function (and any function defined in ChildContext) is undefined on any refresh.
Some Details:
It is not a spelling error, or 'can't find file' error.
The only thing I am currently storing in session is the auth token.
Everything from state is defined (even if in its INITIAL_STATE form)
Wherever I call the undefined function, I use async/await
I do not actually use the Consumer anywhere
The AppWrapper, GlobalContext and ChildContext are all nested inside a context folder. App.js is in the root directory.
I have a certain Context setup similar to this
const DataContext = createContext({ data: null });
const getData = (key) => {
switch(key) {
case 1:
return "Hello"
case 2:
return " World"
default:
return null
}
}
export const DataProvider = ({ id, children }) => {
const data = useMemo(() => {
return getData(id);
}, [id]);
return (
<DataContext.Provider
value={{
data,
}}
>
{children}
</DataContext.Provider>
);
};
export default DataContext
And child components that use it like this
const HelloComponent = () => {
return <DataProvider id={1}>
{
// children are components that useContext(DataContext) and expect data to be "Hello"
}
</DataProvider>
}
Now I need to do this
const HelloWorldComponent = () => {
return (
<DataProvider id={1}>
<DataProvider id={2}>
{
// children are components that need to read both Hello and World
}
</DataProvider>
</DataProvider>
);
};
Need to provide all parent context's data of one single Context definition to a set of children
I know useContext can only read the closest parent of a given ContextType, so I'm not sure of how to proceed with this.
You can't use two context providers of the same type and have children receive from both. The Context API is such that children components receive the context value from the closest context provider to them in the React tree.
You can, however, use a single provider that instead returns the getData function. Here's an example using a single DataProvider and a custom React hook to provide the "id" value.
Data.Context.js
import { createContext, useContext } from "react";
const DataContext = createContext({
getData: () => {}
});
const getData = (key) => {
switch (key) {
case 1:
return "Hello";
case 2:
return " World";
default:
return null;
}
};
export const useDataContext = (id) => useContext(DataContext).getData(id);
const DataProvider = ({ children }) => {
return (
<DataContext.Provider value={{ getData }}>{children}</DataContext.Provider>
);
};
export default DataProvider;
index.js
<DataProvider>
<App />
</DataProvider>
Child component
import { useDataContext } from "./Data.Context";
const Child = () => {
const data1 = useDataContext(1);
const data2 = useDataContext(2);
return (
<div>
<div>DataProvider 1: {data1}</div>
<div>DataProvider 2: {data2}</div>
</div>
)
}
What is the correct way to create redux store within useEffect?
I have a use case that a child App.js will be exported as web-component (e.g. a javascript file), then embed (e.g. reference this file in html header) by a parent react app.
One flow is that the parent app will render App.js twice or N times. I put store creation inside useEffect + empty dependency, so this will be called once, regardless how many times the parent app renders the child app.
Once my store is ready, I render rest of the component + pass the store to context.
In Component.js, I look up the state and want to use them.
The error I have:
https://github.com/facebookincubator/redux-react-hook/blob/master/src/create.ts
const state = store.getState();, getState does not exist. It seems that either store is not ready when I tried to look up the state.
So I wonder what is the correct way to create store within useEffect?
App.js
export default function App() {
const store = useRef(null);
const [storeReady, setStoreReady] = useState(false);
const [token, setToken] = useState('');
useEffect(() => {
store.current = createReduxStore();
setStoreReady(true);
return () => {
store.current = null;
};
}, []);
useEffect(() => {
if (storeReady) {
// get token and set in state
token(auth())
}
}, [auth, storeReady]);
// store.current
if (store.current !== null) {
//test
console.log(
'++++++ store.current not null, condi',
store.current !== null,
store.current
);
return (
<>
<StoreContext.Provider value={store.current}>
<MyApp
token={token}
/>
</StoreContext.Provider>
</>
);
} else {
return null;
}
}
Store.js
export function createReduxStore(initialState = {}) {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
return store;
}
Component.js
export default function MyApp() {
const mapState = useCallback(
state => ({
history:
state !== undefined && state.idv !== undefined ? state.idv.history : [],
status:
state !== undefined && state.idv !== undefined
? state.idv.status
: initialStatus,
error:
state !== undefined && state.idv !== undefined ? state.idv.error : ''
}),
[]
);
const {history, status, error} = useMappedState(mapState);
}
// https://github.com/facebookincubator/redux-react-hook/blob/master/src/create.ts
function useMappedState<TResult>(
mapState: (state: TState) => TResult,
equalityCheck: (a: TResult, b: TResult) => boolean = defaultEqualityCheck,
): TResult {
const store = useContext(StoreContext);
if (!store) {
throw new MissingProviderError();
}
// We don't keep the derived state but call mapState on every render with current state.
// This approach guarantees that useMappedState returns up-to-date derived state.
// Since mapState can be expensive and must be a pure function of state we memoize it.
const memoizedMapState = useMemo(() => memoizeSingleArg(mapState), [
mapState,
]);
// getState is undefined
const state = store.getState();
const derivedState = memoizedMapState(state);
Update 1, but not working #backtick
export default function App() {
const duckStore = { getState() {} };
const store = useRef(duckStore);
const [token, setToken] = useState('');
useEffect(() => {
store.current = createReduxStore();
return () => {
store.current = duckStore;
};
}, []);
// store.current
if (store.current !== null) {
return (
<>
<StoreContext.Provider value={store.current}>
<MyApp />
</StoreContext.Provider>
</>
);
} else {
return null;
}
}
Store.js
export function createReduxStore(initialState = {}) {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
return store;
}
Component.js
export default function MyApp() {
// There is no store variable here, useMappedState is able to get the store value.
const mapState = useCallback(
state => ({
history:
state !== undefined && state.idv !== undefined ? state.idv.history : [],
status:
state !== undefined && state.idv !== undefined
? state.idv.status
: initialStatus,
error:
state !== undefined && state.idv !== undefined ? state.idv.error : ''
}),
[]
);
const {history, status, error} = useMappedState(mapState);
}
useMappedState calls the store.getState() on every render, even the first one when the store isn't ready. Just duck-type the store ref's initial value:
// module scope
const duckStore = { getState() {} };
// in component
const store = useRef(duckStore);
// in useEffect cleanup
return () => { store.current = duckStore; };
I am using a custom hook useInstantSearch in my component.
When I wrap it in useCallback to I get the following error:
React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.
This is the code:
const [searchTerm, setSearchTerm] = useState<string>(searchQuery);
const handleDebouncedSearch = useCallback(
useInstantSearch(searchTerm, (search, cancelTrigger, searchStillValid) => {
console.log('instant search', search);
}),
[]
);
useEffect((): void => {
handleDebouncedSearch(searchTerm);
}, [searchTerm, handleDebouncedSearch]);
So effectively to send the updated search term to a child component for display only then the parent handles the debouncing of the search when that term changes.
search, cancelTrigger, searchStillValid
Are not part of the parent component, they are part of useInstantSearch.
Is this a warning I can ignore?
import { useEffect, useRef } from 'react';
import { CancelTrigger } from '../../../ars/api/api.cancel';
const DELAY_SEARCH_MS = 300;
interface InstantSearchOnChange {
(search: string, cancelTrigger: CancelTrigger, resultStillValid: { (): boolean }): void;
}
/**
* Helper to delay request until user stop typing (300ms), support deprecated requests (cancel and helper to not update the state), or unmounted component.
*/
export default function useInstantSearch(initialSearch: string, onChange: InstantSearchOnChange): { (value: string): void } {
const search = useRef<string>(initialSearch);
const requests = useRef<CancelTrigger[]>([]);
const mounted = useRef<boolean>(true);
useEffect(() => {
return (): void => {
mounted.current = false;
};
}, []);
return value => {
search.current = value;
setTimeout(() => {
if (search.current === value) {
requests.current = requests.current.filter(r => !r.cancel());
const trigger = new CancelTrigger();
requests.current.push(trigger);
onChange(value, trigger, () => search.current === value && mounted.current);
}
}, DELAY_SEARCH_MS);
};
}
Since you're using some external function, you can simply ignore the message:
useCallback(
useInstantSearch(...)
, []) // eslint-disable-line react-hooks/exhaustive-deps
However, you should be using it like:
const [searchTerm, setSearchTerm] = useState<string>(searchQuery);
const handleDebouncedSearch = useCallback(() => { // -> this
useInstantSearch(searchTerm, (search, cancelTrigger, searchStillValid) => {
console.log('instant search', search);
})
}, [searchTerm]); // eslint-disable-line react-hooks/exhaustive-deps
Eslint comment is required here because, you're using callback inside useInstantSearch as there's no way to inject them as dependency.
You can ignore it if you don't mind the stale closures you can do it that way:
const { useRef, useCallback, useEffect } = React;
const DELAY_SEARCH_MS = 300;
const later = (value, time) =>
new Promise((resolve) =>
setTimeout(() => resolve(value), time)
);
/**
* Helper to delay request until user stop typing (300ms), support deprecated requests (cancel and helper to not update the state), or unmounted component.
*/
function useInstantSearch(onChange) {
const timeout = useRef();
const mounted = useRef(true);
useEffect(() => {
return () => {
mounted.current = false;
};
}, []);
return useCallback(
(value) => {
clearTimeout(timeout.current); //cancel other
timeout.current = setTimeout(() => {
const current = timeout.current;
onChange(
value,
() =>
//comparing timeout.current with current
// async function may not be the last to resolve
// this is important when you want to set state based
// on an async result that is triggered on user input
// user types "a" and then "b" if 2 async searches start
// "a" and "ab" and "a" is so slow it will resolve after "ab"
// then state result will be set for "ab" first and then with "a"
// causing UI to be out of sync because user searched for "ab"
// but results for "a" are shown
timeout.current === current && mounted.current
);
}, DELAY_SEARCH_MS);
},
[onChange]
);
}
const App = () => {
const handler1 = useCallback(
(value) => console.log('handler1:', value),
[]
);
const handler2 = useCallback(
(value) => console.log('handler2:', value),
[]
);
const handler3 = useCallback((value, shouldResolve) => {
console.log('starting async with:', value);
return later(
value,
value.length === 1 ? 1000 : 800
).then(
(resolve) =>
shouldResolve() &&//you can opt not to set state here
console.log('resolved with', resolve)
);
}, []);
const debounced1 = useInstantSearch(handler1);
const debounced2 = useInstantSearch(handler2);
const debounced3 = useInstantSearch(handler3);
[1, 2, 3].forEach(
(num) =>
setTimeout(() => {
debounced1(num);
debounced2(num * 2);
}, 100) //lower than the 300 of debounce
);
//both callbacks will be called but "a" resolves after "ab" since
// "ab" was the last to be requested it will be the only one that logs
// resolved with
setTimeout(() => debounced3('a'), 500);
setTimeout(() => debounced3('ab'), 1500);
return 'hello world (check console)';
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
There may be a solution to your problem but without knowing what useInstantSearch is it's impossible to provide one.
My guess is that you should use useCallback inside useInstantSearch but since that code is missing from your question I can only guess.