How to create pages from non-seriazable data(functions) - javascript

I have this JavaScript data file(src/test/test.js):
module.exports = {
"title": "...",
"Number": "number1",
"Number2": ({ number1 }) => number1 / 2,
}
I want to pass this file verbatim(functions preserved) to a page, so that the page can use that data to build itself. I already have the page template and everything else sorted out, I just need to find a way to pass this into the page.
The first approach I tried is requireing this file in gatsby-node.js and then passing it as pageContext.
gatsby-node.js
const path = require('path');
exports.createPages = ({actions, graphql}) => {
const { createPage } = actions;
return graphql(`
query loadQuery {
allFile(filter: {sourceInstanceName: {eq: "test"}}) {
edges {
node {
relativePath
absolutePath
}
}
}
}
`).then(result => {
if (result.errors) {
throw result.errors;
}
for (const node of result.data.allFile.edges.map(e => e.node)) {
const data = require(node.absolutePath);
createPage({
path: node.relativePath,
component: path.resolve('./src/templates/test.js'),
context: data,
});
}
});
};
gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
name: `test`,
path: `${__dirname}/src/test/`,
},
},
],
}
src/templates/test.js
import React from 'react';
const index = ({ pageContext }) => (
<p>{pageContext.Number2()}</p>
);
export default index;
However, I get this warning when running the dev server:
warn Error persisting state: ({ number1 }) => number1 / 2 could not be cloned.
If I ignore it and try to use the function anyway, Gatsby crashes with this error:
WebpackError: TypeError: pageContext.Number2 is not a function
After searching for a while, I found this:
The pageContext was always serialized so it never worked to pass a function and hence this isn't a bug. We might have not failed before though.
- Gatsby#23675
which told me this approach wouldn't work.
How could I pass this data into a page? I've considered JSON instead, however, JSON can't contain functions.
I've also tried finding a way to register a JSX object directly, however I couldn't find a way.

Regarding the main topic, as you spotted, can't be done that way because the data is serialized.
How could I pass this data into a page? I've considered JSON instead,
however, JSON can't contain functions.
Well, this is partially true. You can always do something like:
{"function":{"arguments":"a,b,c","body":"return a*b+c;"}}
And then:
let func = new Function(function.arguments, function.body);
In this case, you are (de)serializing a JSON function, creating and casting a function based on JSON parameters. This approach may work in your scenario.
Regarding the JSX, I guess you can try something like:
for (const node of result.data.allFile.edges.map(e => e.node)) {
const data = require(node.absolutePath);
createPage({
path: node.relativePath,
component: path.resolve('./src/templates/test.js'),
context:{
someComponent: () => <h1>Hi!</h1>
},
});
}
And then:
import React from 'react';
const Index = ({ pageContext: { someComponent: SomeComponent} }) => (
return <div><SomeComponent /></div>
);
export default index;
Note: I don't know if it's a typo from the question but index should be capitalized as Index
In this case, you are aliasing the someComponent as SomeComponent, which is a valid React component.

Related

How to run React custom hook conditionally or inside a function

I have a custom hook to redirect users to edit page. On index page I can duplicate items and delete. I can redirect users after duplicate, but the problem is when I delete an item, this custom hook redirects users to edit page again. So I need to find a way to make it work conditionally.
Custom hook:
export default function useDuplicateItem(url: string) {
const { sendRequest: duplicate } = useHttpRequest();
const duplicateItem = useCallback(
(data) => {
duplicate([
{
url: `/api/server/${url}`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data,
},
]);
},
[duplicate, url]
);
useRedirectEditPage(url); // This causes the problem
return duplicateItem;
}
index page:
const duplicateItem = useDuplicateItem('documents');
// custom hook rendered here, which is not correct. I want to run it when duplicate function runs.
const duplicate = useCallback(() => {
const data = {
name: copiedName,
sources: singleDocument?.sources,
document: singleDocument?.document,
tool: singleDocument?.tool,
access: singleDocument?.access,
};
duplicateItem(data);
}, [copiedName, duplicateItem, singleDocument]);
useRedirectEditPage:
export default function useRedirectEditPage(slug: string) {
const { saveResponses, setSaveResponses, setHeaderStates } =
useAdminContext();
const router = useRouter();
useEffect(() => {
const statusCodes: number[] = [];
let id;
saveResponses.forEach((value) => {
statusCodes.push(value?.status);
id = value?.id;
});
if (statusCodes.length && id) {
if (statusCodes.includes(404)) {
setHeaderStates((prev) => ({
...prev,
canBeSaved: false,
}));
} else {
router.push(`/admin/${slug}/edit/${id}`);
setSaveResponses(new Map());
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveResponses, router, setSaveResponses]);
}
saveResponses state is coming after I make any request to server, and I am able to get id to redirect users. I use new Map() to set data inside saveResponses.
From the react docs:
Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function,
before any early returns. By following this rule, you ensure that
Hooks are called in the same order each time a component renders.
That’s what allows React to correctly preserve the state of Hooks
between multiple useState and useEffect calls. (If you’re curious,
we’ll explain this in depth below.)
React relies on the order in which Hooks are called to know which setState corresponds to which state, calling them inside a condition will mess up the previous mechanism.
I would recommend to read the following: https://reactjs.org/docs/hooks-rules.html#explanation

Error: Maximum update depth exceeded. Component calls setState inside useEffect,..., or one of the dependencies changes on every render

I have error like in title. But I don't know how my dependencie change every render because it is in useEffect. I suspected it was related to the map function so I moved it higher in the hierarchy to "injectEndpoints"->"transformResponse" but that didn't help. I don't know how to change it or maybe that's not the point?
export const parametersApi = myApi
.injectEndpoints({
endpoints: (builder) => ({
getParameters: builder.query<
ExtendedParameter[],
string
>({
query: (id) =>
`/${id}/parameters`,
providesTags: (_result, _error, id) => [
{
type: `Parameters`,
id: `PLIST_${id}`,
},
],
transformResponse: (response: parameter[]) => {
return response.map((param) => {
return { id: param.name, ...param };
});
},
}),
....
export const {
useGetParametersQuery } = parametersApi;
The file in which the error occurs:
const selectedParameters =
useGetParametersQuery(master.id, {
skip: !master,
}).data || [];
...
useEffect(() => {
console.log("selectedParameters", selectedParameters);
setParameters(selectedParameters);
}, [selectedParameters]);
console.log("selectedParameters", selectedParameters);
|| []
creates a new array every render while there is no .data yet - so selectedParameters will be a new reference and trigger an useEffect every time.
Do something like const EMPTY_ARR = [] outside your component and do || EMPTY_ARR to keep a stable reference here.

Nested namespaces in i18n

My application is rather large, so to have a more organized translation file I want to use nasted namespaces. Example:
{
"contract": {
"index": {
"pageTitle": "Contract"
}
}
The problem with this is when I'm accessing it. With the help of this question I found out I can access the keys inside index by using it as below:
const { t, i18n } = useTranslation('contract', { useSuspense: false });
...
t('index.pageTitle')
The problem is It seems rather unecessary to prefix index. to every key I want to access. What I would like to do is import the namespace index instead of contract, and use it as below:
const { t, i18n } = useTranslation('contract:index', { useSuspense: false });
...
t('pageTitle')
Which doesn't work. I tried contract.index as well. In the official documentation I found nothing about nesting. Is it possible to accomplish what I'm trying to do or will I have to stick with prexifing every key?
Nested namespaces are not supported.
You can decorate the useTranslation hook to provide this extended functionality for pages in the namespace.
import { useTranslation as useTranslationBase } from "react-i18next";
const useTranslation = (ns, page, props={}) => {
const trans = useTranslationBase(ns, props);
return {
...trans,
t: (keys, options) => {
let _keys = keys;
if (!Array.isArray(keys)) _keys = [String(keys)];
_keys = _keys.map(key =>`${page}.${key}`)
return trans.t(_keys, options)
}
}
}
Usage
export default function () {
const { t } = useTranslation('contract', 'index');
return <div>{t(["pageTitle"])}-{t("pageTitle")}</div>
}

Jest Mock returns undefined instead of data

I'm trying to mock a function and not sure what i'm doing wrong here. I have this function "getGroups"
getGroups:
export const getGroups = async () => {
try {
const groupApiUrl = getDashboardPath(GROUPS_TAB_INDEX);
const data = await fetch(groupApiUrl, { cache: 'force-cache' });
const userData = await data.json();
return userData;
} catch (error) {
throw Error(error);
}
};
___mocks___/getGroups.js:
export default async () => {
return {
groups: [
{ id: 1, name: 'Data1' },
{ id: 2, name: 'Data2' }
]
};
};
getGroups.test.js:
jest.mock('./getGroups.js');
// eslint-disable-next-line import/first
import { getGroups } from './getGroups';
const fakeRespose = {
groups: [
{ id: 1, name: 'Data1' },
{ id: 2, name: 'Data2' }
]
};
describe('getGroups', () => {
it('returns data', async () => {
const data = await getGroups();
console.log('DATA', data); <---- UNDEFINED?
expect(data).toBeDefined();
expect(data).toMatchObject(fakeRespose);
});
it('handles error', async () => {
// const data = await getGroups();
await getGroups().toThrow('Failed');
});
});
What are you doing wrong here?
Default export in your mock instead of named as in the implementation
In your implementation you're using named export and you're importing { getGroups } so to make it work you need to change your mock like this
__mocks__\getGroups.js
export const getGroups = async () => {
return {
groups: [
{ id: 1, name: 'Data1' },
{ id: 2, name: 'Data2' }
]
};
};
working example
TL;DR
Testing mock
There is no point at all to test mock function. This does not proves your implementation is working. Even if you change your implementation your tests will still pass.
Use mocks only for the dependencies of your implementation
Use jest.genMockFromModule
It will create jest.fn() for each of the module's exported methods and will preserve the constants, allowing you to change the return value/implementation for some test cases and will also be able to write assertions if the function have been called
__mocks__\getGroups.js
const mock = jest.genMockFromModule('../getGroups');
mock.getGroups.mockResolvedValue({
groups: [
{ id: 1, name: 'Data1' },
{ id: 2, name: 'Data2' }
]
})
module.exports = mock;
Jest will automatically hoist jest.mock calls (read more...)
So you can safely leave the import statements first and then call jest.mock
From Jest Docs, here's an example of a Mock.
jest.mock('../moduleName', () => {
return jest.fn(() => 42);
});
// This runs the function specified as second argument to `jest.mock`.
const moduleName = require('../moduleName');
moduleName(); // Will return '42';
In your case data is undefined, because you haven't actually supplied a mocked implementation for the function or the mock hasn't worked and you're still calling the original function.
Example Reference: https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options
However, in your simple case you could also solve this with a spy, either jest.spyOn or jest.fn(). Here are two solutions to what you're trying to achieve. You can look at the code and run it here: https://repl.it/repls/FairUnsungMice
UPDATE after comment:
Manual mocks are defined by writing a module in a __mocks__/
subdirectory immediately adjacent to the module. For example, to mock
a module called user in the models directory, create a file called
user.js and put it in the models/__mocks__ directory. Note that the
__mocks__ folder is case-sensitive, so naming the directory __MOCKS__ will break on some systems.
Double check the naming, directory structure & type of exports you've setup - they should match. Also, it's worth checking this out: https://github.com/facebook/jest/issues/6127 - looks like an open issue with jest. If you need a solution, look at using a different approach as I mentioned.
Reference: https://jestjs.io/docs/en/manual-mocks

How do I handle deletes in react-apollo

I have a mutation like
mutation deleteRecord($id: ID) {
deleteRecord(id: $id) {
id
}
}
and in another location I have a list of elements.
Is there something better I could return from the server, and how should I update the list?
More generally, what is best practice for handling deletes in apollo/graphql?
I am not sure it is good practise style but here is how I handle the deletion of an item in react-apollo with updateQueries:
import { graphql, compose } from 'react-apollo';
import gql from 'graphql-tag';
import update from 'react-addons-update';
import _ from 'underscore';
const SceneCollectionsQuery = gql `
query SceneCollections {
myScenes: selectedScenes (excludeOwner: false, first: 24) {
edges {
node {
...SceneCollectionScene
}
}
}
}`;
const DeleteSceneMutation = gql `
mutation DeleteScene($sceneId: String!) {
deleteScene(sceneId: $sceneId) {
ok
scene {
id
active
}
}
}`;
const SceneModifierWithStateAndData = compose(
...,
graphql(DeleteSceneMutation, {
props: ({ mutate }) => ({
deleteScene: (sceneId) => mutate({
variables: { sceneId },
updateQueries: {
SceneCollections: (prev, { mutationResult }) => {
const myScenesList = prev.myScenes.edges.map((item) => item.node);
const deleteIndex = _.findIndex(myScenesList, (item) => item.id === sceneId);
if (deleteIndex < 0) {
return prev;
}
return update(prev, {
myScenes: {
edges: {
$splice: [[deleteIndex, 1]]
}
}
});
}
}
})
})
})
)(SceneModifierWithState);
Here is a similar solution that works without underscore.js. It is tested with react-apollo in version 2.1.1. and creates a component for a delete-button:
import React from "react";
import { Mutation } from "react-apollo";
const GET_TODOS = gql`
{
allTodos {
id
name
}
}
`;
const DELETE_TODO = gql`
mutation deleteTodo(
$id: ID!
) {
deleteTodo(
id: $id
) {
id
}
}
`;
const DeleteTodo = ({id}) => {
return (
<Mutation
mutation={DELETE_TODO}
update={(cache, { data: { deleteTodo } }) => {
const { allTodos } = cache.readQuery({ query: GET_TODOS });
cache.writeQuery({
query: GET_TODOS,
data: { allTodos: allTodos.filter(e => e.id !== id)}
});
}}
>
{(deleteTodo, { data }) => (
<button
onClick={e => {
deleteTodo({
variables: {
id
}
});
}}
>Delete</button>
)}
</Mutation>
);
};
export default DeleteTodo;
All those answers assume query-oriented cache management.
What if I remove user with id 1 and this user is referenced in 20 queries across the entire app? Reading answers above, I'd have to assume I will have to write code to update the cache of all of them. This would be terrible in long-term maintainability of the codebase and would make any refactoring a nightmare.
The best solution in my opinion would be something like apolloClient.removeItem({__typeName: "User", id: "1"}) that would:
replace any direct reference to this object in cache to null
filter out this item in any [User] list in any query
But it doesn't exist (yet)
It might be great idea, or it could be even worse (eg. it might break pagination)
There is interesting discussion about it: https://github.com/apollographql/apollo-client/issues/899
I would be careful with those manual query updates. It looks appetizing at first, but it won't if your app will grow. At least create a solid abstraction layer at top of it eg:
next to every query you define (eg. in the same file) - define function that clens it properly eg
const MY_QUERY = gql``;
// it's local 'cleaner' - relatively easy to maintain as you can require proper cleaner updates during code review when query will change
export function removeUserFromMyQuery(apolloClient, userId) {
// clean here
}
and then, collect all those updates and call them all in final update
function handleUserDeleted(userId, client) {
removeUserFromMyQuery(userId, client)
removeUserFromSearchQuery(userId, client)
removeIdFrom20MoreQueries(userId, client)
}
For Apollo v3 this works for me:
const [deleteExpressHelp] = useDeleteExpressHelpMutation({
update: (cache, {data}) => {
cache.evict({
id: cache.identify({
__typename: 'express_help',
id: data?.delete_express_help_by_pk?.id,
}),
});
},
});
From the new docs:
Filtering dangling references out of a cached array field (like the Deity.offspring example above) is so common that Apollo Client performs this filtering automatically for array fields that don't define a read function.
Personally, I return an int which represents the number of items deleted. Then I use the updateQueries to remove the document(s) from the cache.
I have faced the same issue choosing the appropriate return type for such mutations when the rest API associated with the mutation could return http 204, 404 or 500.
Defining and arbitrary type and then return null (types are nullable by default) does not seem right because you don't know what happened, meaning if it was successful or not.
Returning a boolean solves that issue, you know if the mutation worked or not, but you lack some information in case it didn't work, like a better error message that you could show on FE, for example, if we got a 404 we can return "Not found".
Returning a custom type feels a bit forced because it is not actually a type of your schema or business logic, it just serves to fix a "communication issue" between rest and Graphql.
I ended up returning a string. I can return the resource ID/UUID or simply "ok" in case of success and return an error message in case of error.
Not sure if this is a good practice or Graphql idiomatic.

Categories