memoized a closure and render callback with useCallback - reactjs

let's I have a graphql mutation component, that I reuse in many places
const MarkAsViewed =({ type = 1, children }) => {
const markAsViewed = (commitMutation) => (type) => {
return commitMutation({
variables: { type }
});
};
return (
<MarkAsViewedMutation
mutation={MARK_AS_VIEWED_MUTATION}
variables={{
type,
}}
>
{
(commitMutation, { error, loading }) => children({
markAsViewed: markAsViewed(commitMutation)
})
}
</MarkAsViewedMutation>
);
};
however since markAsViewed is a closure function, it will always return different function with different ref which means different for react.
this makes the child component to have to do a useCallback like:
const alwaysSameRefFunc = useCallback(()=>{ markAsViewed(), []}
above works but creates 2 problems:
I get linter warning saying I should add markAsViewed as dependency blah blah. which I cannot, because it triggers infinite loop (since it's different ref every time)
everyone that uses <MarkAsViewed /> component will need to manually memoirzation
Ideally this is what I want, but it's an invalid code, because "markAsViewed" is not a react component and cannot have useCallback
const markAsViewed = (commitMutation) => useCallback((type) => {
return commitMutation({
variables: { type }
});
}, []);
any idea how can I solve the issue?
note: we are not ready to update Apollo version to have hoook yet

Does the following work?
const markAsViewed = commitMutation => type => {
return commitMutation({
variables: { type },
});
};
const MarkAsViewed = ({ type = 1, children }) => {
const fn = useCallback(
(commitMutation, { error, loading }) =>
children({
markAsViewed: markAsViewed(commitMutation),
}),
[children]
);
return (
<MarkAsViewedMutation
mutation={MARK_AS_VIEWED_MUTATION}
variables={{
type,
}}
>
{fn}
</MarkAsViewedMutation>
);
};
I'm not sure if that will work because it still depends on children, if that causes unnesesary renders then maybe post how MarkAsViewed component is rendered.

Related

Loader that doesn't render children unless data is fetched

I am trying to make a component that renders "children" prop only "and only if" a boolean is true, now i noticed if i do something like this
const QueryLoader = (props) => {
if (!props.isSuccess) return <h2>Loading</h2>;
return props.children;
};
and use it as follows
const Main = (props) => {
const {isSuccess,data} = fetcher("api");
return (
<QueryLoader isSuccess={isSuccess}>
<div>{data.arrayOfData.innerSomething}</div>
</QueryLoader>
);
};
the data.arrayOfData.innerSomething is still triggered which is causing me issues, i thought about instead of sending children i send a component as a function and then call it inside the QueryLoader but i dont know if this has any side-effects.
Any suggestions?
This is called render prop pattern:
const QueryLoader = ({ isSuccess, children }) => {
return isSuccess ? children() : <h2>Loading</h2>;
};
const Main = () => {
const { isSuccess, data } = fetcher("api");
return (
<QueryLoader isSuccess={isSuccess}>
{() => <div>{data.arrayOfData.innerSomething}</div>}
</QueryLoader>
);
};
For data fetching I hightly recommend using react-query library.

react-query with rules of hook broken error

I am trying to extract my API calls using react-query into a reusable hook. The parameters I need to send to this hook are moduleName and value. For some reason, I get an error that I need to follow hooks rules.
Please advice.
This is my code:
export const useAutoSave = () => {
const fetcher = useCallback(
(
moduleName: ISourceLoaderEditTabs,
value: Partial<ISourceConfigurationEdit[ISourceLoaderEditTabs]>,
saveUrl = '',
) => {
const handleSaveSourceDetailsMutation = useMutation(
(data: ISourceConfigurationEdit) =>
saveUrl
? postSaveStageRaw(`${POST_SAVE_STAGE_RAW}?${saveUrl}`, data)
: saveSourceDetails(data),
);
const sourceId = sessionStorage.getItem('sourceId');
const sourceDetail = queryClient.getQueryData([
'getSourcesDetail',
Number(sourceId),
]);
handleSaveSourceDetailsMutation.mutate(
{
...(sourceDetail as ISourceConfigurationEdit),
[moduleName]: {
...(sourceDetail as ISourceConfigurationEdit)[moduleName],
...value,
},
},
{
onSuccess: async (data) => {
queryClient.setQueryData(
['getSourcesDetail', Number(sourceId)],
data,
);
},
},
);
},
[],
);
return [fetcher];
};
Then in my component I use it as
const [fetch] = useAutoSave();
fetch('abc', {
name:'a2441918'
})
Code snippet : Stackblitz: https://stackblitz.com/edit/react-q8uvse?file=src%2Fhooks.js
you cannot call useMutation inside useCallback. Also, you don't need to. useMutation returns one object with two functions - mutate and mutateAsync, that you can invoke when you want to call invoke your mutation. So your custom hook very likely should only return whatever useMutation returns. The fist argument to useMutation is the mutateFn - the function that is called when you invoke mutate or mutateAsync, and you can also pass one parameters object there:
const useAutoSave = () => {
return useMutation(
({ moduleName, value, saveUrl }) => saveUrl
? postSaveStageRaw(`${POST_SAVE_STAGE_RAW}?${saveUrl}`, data)
: saveSourceDetails(data),
)
}
you can then invoke it via:
const { mutate } = useAutoSave()
<button onClick={() => {
mutate({ moduleName: 'something, value: 'somethingElse' })
}}>Save</button>
The issue as it states in the error log - usage of useMutation
const handleSaveSourceDetailsMutation = useMutation(
(data: ISourceConfigurationEdit) =>
saveUrl
? postSaveStageRaw(`${POST_SAVE_STAGE_RAW}?${saveUrl}`, data)
: saveSourceDetails(data),
);
This useMutation hook needs to be outside of the useCallback. This also means that the saveUrl and other params need to be refactored.
export const useAutoSave = () => {
const i_dont_know = useMutation(x,x,x,x); // hooks can't be called in regular functions
}
Rules of hook for reference: https://reactjs.org/docs/hooks-rules.html

React When does rendering happen

My project use dvajs(Based on redux and redux-saga), The code below is to send a request after clicking the button, change the status through connect, and then call the ant design component message.error an message.success(Similar to alert) to remind
import type { Dispatch } from 'umi';
import ProForm, { ProFormText } from '#ant-design/pro-form';
import { message } from 'antd';
const tip = (type: string, content: string) => {
if (type === 'error') message.error(content, 5);
else message.success(content, 5);
};
const RegisterFC: React.FC<RegisterProps> = (props) => {
const { registerResponseInfo = {}, submitting, dispatch } = props;
const { status } = registerResponseInfo;
const handleSubmit = (values: RegisterParamsType) => {
dispatch({
type: 'register/register',
payload: { ...values },
});
};
return (
<div>
<ProForm
onFinish={(values) => {
handleSubmit(values as RegisterParamsType);
return Promise.resolve();
}}
>
<ProFormText/>
...
{
status === '1' && !submitting && (
tip('error',
intl.formatMessage({
id: 'pages.register.status1.message',
defaultMessage: 'error'
})
)
)
}
<<ProForm>/>
</div>
)
}
const p = ({ register, loading }: { register: RegisterResponseInfo, loading: Loading; }) => {
console.log(loading);
return {
registerResponseInfo: register,
submitting: loading.effects['register/register'],
};
};
export default connect(p)(RegisterFC);
When I click the button, the console prompts:
Warning: Render methods should be a pure function of props and state;
triggering nested component updates from render is not allowed. If
necessary, trigger nested updates in componentDidUpdate.
Doesn't the component re-render when the state changes? Does the tip function change the state?
Solution: Call tip Outside of return
tip is just a function that you are calling. You should call it outside of the return JSX section of your code. I think it makes the most sense to call it inside of a useEffect hook with dependencies on status and submitting. The effect runs each time that status or submitting changes. If status is 1 and submitting is falsy, then we call tip.
const RegisterFC: React.FC<RegisterProps> = (props) => {
const { registerResponseInfo = {}, submitting, dispatch } = props;
const { status } = registerResponseInfo;
const handleSubmit = (values: RegisterParamsType) => {
dispatch({
type: 'register/register',
payload: { ...values },
});
};
React.useEffect(() => {
if (status === '1' && !submitting) {
tip('error',
intl.formatMessage({
id: 'pages.register.status1.message',
defaultMessage: 'error'
})
);
}
}, [status, submitting]);
return (
<div>...</div>
)
}
Explanation
Render methods should be a pure function of props and state
The render section of a component (render() in class component or return in a function component) is where you create the JSX (React HTML) markup for your component based on the current values of props and state. It should not have any side effects. It creates and returns JSX and that's it.
Calling tip is a side effect since it modifies the global antd messsage object. That means it shouldn't be in the render section of the code. Side effects are generally handled inside of useEffect hooks.
You are trying to conditionally render tip like you would conditionally render a component. The problem is that tip is not a component. A function component is a function which returns a JSX Element. tip is a void function that returns nothing, so you cannot render it.

React Query useMutation set mutationKey dynamically

In my React project using React Query, I have a functional component MoveKeywordModal such that:
when it first loads, it fetches from API endpoint api/keyword_lists to fetch a bunch of keywordLists data. For each of these keywordLists, call it list, I create a clickable element.
When the clickable element (wrapped in a HoverWrapper) gets clicked, I want to send a POST API request to api/keyword_lists/:list_id/keyword_list_items/import with some data.
where :list_id is the id of the list just clicked.
export const MoveKeywordModal = ({
setShowMoveKeywordModal,
keywordsToMove
}) => {
const { data: keywordLists } = useQuery('api/keyword_lists', {})
const [newKeywordList, setNewKeywordList] = useState({})
const { mutate: moveKeywordsToList } = useMutation(
`api/keyword_lists/${newKeywordList.id}/keyword_list_items/import`,
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
const availableKeywordLists = keywordLists
.filter(l => l.id !== activeKeywordList.id)
.map(list => (
<HoverWrapper
id={list.id}
onClick={() => {
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList, data: keywordsToMove }
})
}}>
<p>{list.name}</p>
</HoverWrapper>
))
return (
<>
<StyledModal
isVisible
handleBackdropClick={() => setShowMoveKeywordModal(false)}>
<div>{availableKeywordLists}</div>
</StyledModal>
</>
)
}
Despite calling setNewKeywordList(list) in the onClick of the HoverWrapper, it seems the newKeywordList.id is still not defined, not even newKeywordList is defined.
What should I do to fix it?
Thanks!
react doesn’t perform state updates immediately when you call the setter of useState - an update is merely 'scheduled'. So even though you call setNewKeywordList, the newKeywordList will not have the new value in the next line of code - only in the next render cycle.
So while you are in your event handler, you’ll have to use the list variable:
setNewKeywordList(list)
moveKeywordsToList({
variables: { newKeywordList: list, data: keywordsToMove }
})
/edit: I just realized that your call to useMutation is not correct. It doesn’t have a key like useQuery, it has to provide a function as the first argument that takes variables, known as the mutation function:
const { mutate: moveKeywordsToList } = useMutation(
(variables) => axios.post(`api/keyword_lists/${variables.newKeywordList.id}/keyword_list_items/import`),
{
onSuccess: data => {
console.log(data)
},
onError: error => {
console.log(error)
}
}
)
see also: https://react-query.tanstack.com/guides/mutations

Child component not being updated with funcional components

I have a component where I get repositories like below:
useEffect(() => {
loadRepositories();
}, []);
const loadRepositories = async () => {
const response = await fetch('https://api.github.com/users/mcand/repos');
const data = await response.json();
setUserRepositories(data);
};
return (
<Repository repositories={userRepositories} />
)
The child component only receives the repositories and prints them. It's like that:
const Repository = ({ repositories }) => {
console.log('repository rendering');
console.log(repositories);
const [repos, setRepos] = useState(repositories);
useEffect(() => {
setRepos(repositories);
}, [ ]);
const getRepos = () => {
repos.map((rep, idx) => {
return <div key={idx}>{rep.name}</div>;
});
};
return (
<>
<RepositoryContainer>
<h2>Repos</h2>
{getRepos()}
</>
);
};
export default Repository;
The problem is that, nothing is being displayed. In the console.log I can see that there're repositories, but it seems like the component cannot update itself, I don't know why. Maybe I'm missing something in this useEffect.
Since you're putting the repository data from props into state, then then rendering based on state, when the props change, the state of a Repository doesn't change, so no change is rendered.
The repository data is completely handled by the parent element, so the child shouldn't use any state - just use the prop from the parent. You also need to return from the getRepos function.
const Repository = ({ repositories }) => {
const getRepos = () => {
return repositories.map((rep, idx) => {
return <div key={idx}>{rep.name}</div>;
});
};
return (
<>
<RepositoryContainer>
<h2>Repos</h2>
{getRepos()}
</>
);
};
export default Repository;
You can also simplify
useEffect(() => {
loadRepositories();
}, []);
to
useEffect(loadRepositories, []);
It's not a bug yet, but it could be pretty misleading - your child component is named Repository, yet it renders multiple repositories. It might be less prone to cause confusion if you named it Repositories instead, or have the parent do the .map instead (so that a Repository actually corresponds to one repository array item object).
Change this
useEffect(() => {
setRepos(repositories);
}, [ ]);
to
useEffect(() => {
setRepos(repositories);
}, [repositories]);

Resources