I'm new for the RTK Query for redux.
What's the different for auto generated hook in below two ways.
The first way look like correct from the docs but it return 304 network status.
Second way, return 200. working perfectly
1.
const ProjectsList = () => {
const {
data: projects,
isLoading,
isSuccess,
isError,
error,
} = useGetProjectsQuery("projectList") // -- return 304 network status
}
worked fine. but cannot retrieve the object from the store. return.
const {
data: projects,
isLoading,
isSuccess,
isError,
error,
} = useGetProjectsQuery() // -- return 200 network status
Third, the memoized return uninitialize. It seem didn't correct.
// ApiSlice status return uninitialize
import { createSelector, createEntityAdapter } from "#reduxjs/toolkit"
import { apiSlice } from "#/app/api/apiSlice"
const projectsAdapter = createEntityAdapter({})
export const projectsApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getProjects: builder.query({
query: () => "/api/projects",
validateStatus: (response, result) => {
return response.status === 200 && !result.isError
},
transformResponse: (responseData) => {
const loadedProjects = responseData.map((project) => {
project.id = project._id
return project
})
return projectsAdapter.setAll(initialState, loadedProjects)
},
providesTags: (result, error, arg) => {
if (result?.ids) {
return [
{ type: "Project", id: "LIST" },
...result.ids.map((id) => ({ type: "Project", id })),
]
} else return [{ type: "Project", id: "LIST" }]
},
}),
}),
})
export const {
useGetProjectsQuery,
} = projectsApiSlice
export const selectProjectsResult =
projectsApiSlice.endpoints.getProjects.select()
// creates memoized selector
const selectProjectsData = createSelector(
selectProjectsResult,
(projectsResult) => {
console.log("projectsResult: ", projectsResult) // -> { isUninitialized: true, status: "uninitialize" }
return projectsResult.data
}
)
export const {
selectAll: selectAllProjects,
selectById: selectProjectById,
selectIds: selectProjectIds,
} = projectsAdapter.getSelectors(
(state) => selectProjectsData(state) ?? initialState
)
Since your query function is just query: () => "/api/projects" (so, not using the argument in any way), both will make exactly the same request for the same resource.
There is no difference between them and every difference you see is probably something random happening on the server and not bound to either invocation.
As for retrieving from the store, there is a difference however.
Your code
export const selectProjectsResult =
projectsApiSlice.endpoints.getProjects.select()
creates a selector for the cache entry that is created calling useGetProjectsQuery() - if you wanted the cache entry for useGetProjectsQuery("foo"), that would need to be projectsApiSlice.endpoints.getProjects.select("foo").
Please note that there should almost never be any reason to use those selectors with React components - those are an escape hatch if you are not working with React. If you are working with React, use the useGetProjectsQuery hook with selectFromResult.
I am seeing people use select in this fashion quite often recently and I assume this traces back to a tutorial that misunderstood the feature - did you learn that in a tutorial and could you share that tutorial? Maybe I can convince the author to change that part.
Related
I have stitched two schemas together and run on localhost to query it. But the query returns null for the data in the second schema and I am not sure why.
I have the following code to stitch to remote schemas together and run a localhost graphql server to serve it. It should add the linked data from the second schema under cmsMetaData in the main Product data. But cmsMetaData is null.
import { ApolloServer } from 'apollo-server-micro';
import { ApolloServerPluginInlineTraceDisabled, ApolloServerPluginLandingPageLocalDefault } from "apollo-server-core";
import { stitchSchemas } from '#graphql-tools/stitch';
import { delegateToSchema } from '#graphql-tools/delegate';
import { RenameTypes, RenameRootFields } from '#graphql-tools/wrap';
import createRemoteSchema from '../../utils/createRemoteExecutor';
// Configuration for Next.js API Routes
export const config = {
api: {
bodyParser: false,
},
};
// Export as a Next.js API Route
export default async (req, res) => {
// Setup subschema configurations
const productsSubschema = await createRemoteSchema({
url: 'https://schema1.com/graphql/'
});
const cmsSubschema = await createRemoteSchema({
url: 'https://schema2.com/graphql/',
transforms: [
new RenameRootFields(
(operationName, fieldName, fieldConfig) => `strapi_${fieldName}`,
),
new RenameTypes((name) => `Strapi_${name}`),
],
});
// Build the combined schema and set up the extended schema and resolver
const schema = stitchSchemas({
subschemas: [productsSubschema, cmsSubschema],
typeDefs: `
extend type Product {
cmsMetaData: Strapi_Product
}
`,
resolvers: {
Product: {
cmsMetaData: {
selectionSet: `{ id }`,
resolve(product, args, context, info) {
// Get the data for the extended type from the subschema for Strapi
return delegateToSchema({
schema: cmsSubschema,
operation: 'query',
fieldName: 'strapi_product',
args: { where: { SaleorID: product.id } },
context,
info,
});
},
},
},
},
});
// Set up the GraphQL server
const apolloServer = new ApolloServer({
schema,
plugins: [
ApolloServerPluginInlineTraceDisabled(),
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
],
});
await apolloServer.start();
const apolloServerHandler = apolloServer.createHandler({
path: '/api/graphql',
});
// Return the GraphQL endpoint
return apolloServerHandler(req, res);
};
utils/createRemoteExecutor.js is:
import { introspectSchema, wrapSchema } from '#graphql-tools/wrap';
import { print } from 'graphql';
// Builds a remote schema executor function,
// customize any way that you need (auth, headers, etc).
// Expects to recieve an object with "document" and "variable" params,
// and asynchronously returns a JSON response from the remote.
export default async function createRemoteSchema({ url, ...filters }) {
const executor = async ({ document, variables }) => {
const query = print(document);
const fetchResult = await fetch(url, {
method: 'POST',
headers: {
// We can also do Authentication here
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
});
return fetchResult.json();
};
return wrapSchema({
schema: await introspectSchema(executor),
executor,
...filters,
});
}
The query is:
products(first: 100, channel: "default-channel")
{
edges
{
node
{
id
name
cmsMetaData
{
Title
SaleorID
}
}
}
}
In my api.tsx, which I generate using codegen.yaml, Product contains cmsMetaData as follows, which is of type Strapi_Product:
export type Product = Node & ObjectWithMetadata & {
__typename?: 'Product';
...
cmsMetaData?: Maybe<Array<Maybe<Strapi_Product>>>;
...
}
Strapi_Product is as follows which contains Title, SaleorID etc.:
export type Strapi_Product = {
__typename?: 'Strapi_Product';
SaleorID?: Maybe<Scalars['String']>;
Title?: Maybe<Scalars['String']>;
createdAt?: Maybe<Scalars['Strapi_DateTime']>;
publishedAt?: Maybe<Scalars['Strapi_DateTime']>;
updatedAt?: Maybe<Scalars['Strapi_DateTime']>;
};
But the date in GraphQL shows null for cmsMetaData as null:
{
"data": {
"products": {
"edges": [
{
"node": {
"id": "UHJvZHVjdDoxMjc=",
"name": "52-00 Base Plate",
"cmsMetaData": null
}
},
{
"node": {
"id": "UHJvZHVjdDoxMjg=",
"name": "52-01HD Weigh Module",
"cmsMetaData": null
}
}
]
}
}
}
Your problem seems related to this github issue. Most of your code looks totally fine, so I guess it fetches the schema correctly. The cmsMetaData field is null because it did not find anything matching objects using the selection criteria. This unwanted behavior is in the transformation and/or the resolver.
A good starting point for debugging would be to remove the RenameRootFields mutation. Furthermore, this example looks like your use case, it is an excellent step-by-step guide.
I also rebuild your example from an example I found on the internet. You most probably have a typo in one of the field names, that caused a null value for me. Make sure A equals B in the code below. I am guessing the initial fieldName is "Product", so after the transformation, this does not equal "strapi_product" and returns a null value.
const cmsSubschema = await createRemoteSchema({
url: 'https://schema2.com/graphql/',
transforms: [
new RenameRootFields(
(_, fieldName) => `strapi_${fieldName}`,), // A
new RenameTypes((name) => `Strapi_${name}`),
],
});
// Build the combined schema and set up the extended schema and resolver
const schema = stitchSchemas({
subschemas: [productsSubschema, cmsSubschema],
typeDefs: `
extend type Product {
cmsMetaData: Strapi_Product
}
`,
resolvers: {
Product: {
cmsMetaData: {
selectionSet: `{ id }`,
resolve(product, args, context, info) {
// Get the data for the extended type from the subschema for Strapi
return delegateToSchema({
schema: cmsSubschema,
operation: 'query',
fieldName: 'strapi_product', // B
args: { where: { SaleorID: product.id } },
context,
info,
});
},
},
},
},
});
I am having a bit of an issue: I am trying to setup a rewrite in NextJS that will automatically send on the query string to the destination. However, the only examples I can find are with named params. In this case there could be any number of params, so I need a way of making a wildcard (I assume this will be possible?)
What I am looking to do is the following:
/results?param1=a¶m2=b... => https://www.somedomain.com/results?param1=a¶m2=b...
or
/results?house=red&car=blue&money=none => https://www.somedomain.com/results??house=red&car=blue&money=none
rewrites() {
return [
{
source:'/results?:params*',
destination:'https://www.somedomain.com/results?:params*'
},
Of course this doesn't work, so I looked into has but I cannot workout how to make it work without names params
{
source: '/some-page',
destination: '/somewhere-else',
has: [{ type: 'query', key: 'overrideMe' }],
},
You can use a standard page like this below. What I did is when the router is initialized in a useEffect hook it gets the query (object) and then use the objectToQueryString function to turn it back into a url encoded string, then used the router to redirect to the other page
// /pages/some-page.jsx
import { useRouter } from 'next/router'
import { useEffect } from 'react'
const objectToQueryString = (initialObj) => {
const reducer = (obj, parentPrefix = null) => (prev, key) => {
const val = obj[key];
key = encodeURIComponent(key);
const prefix = parentPrefix ? `${parentPrefix}[${key}]` : key;
if (val == null || typeof val === 'function') {
prev.push(`${prefix}=`);
return prev;
}
if (['number', 'boolean', 'string'].includes(typeof val)) {
prev.push(`${prefix}=${encodeURIComponent(val)}`);
return prev;
}
prev.push(Object.keys(val).reduce(reducer(val, prefix), []).join('&'));
return prev;
};
return Object.keys(initialObj).reduce(reducer(initialObj), []).join('&');
};
const Results = () => {
const router = useRouter()
useEffect(() => {
const query = router.query
router.replace(`https://www.somedomain.com/results?${objectToQueryString(query)}`)
}, [router])
return <></>
}
export default Results
I used the objectToQueryString based on Query-string encoding of a Javascript Object
To achieve this behavior, you need to capture the value of the query-parameter like this:
rewrites() {
return [
{
source: "/some-page",
has: [{ type: "query", key: "override_me", value: "(?<override>.*)" }],
destination: "/somewhere-else?override_me=:override",
},
];
}
By default, a rewrite will pass through any query params that the source URL may have.
In your case, you can simply have the following rewrite rule. All query params will be forwarded through the destination URL.
rewrites() {
return [
{
source: '/results',
destination: 'https://www.somedomain.com/results'
}
]
}
I am trying to append action.payload to my state. However, push methods adds action.payload.length to my state instead of appending the entire array! What am I doing wrong?
const initialState = { users: [] };
export const usersSlice = createSlice({
//other code.
,
extraReducers(builder) {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
console.log(current(state));
state.users = state.users.push(...action.payload);
console.log(action.payload);
console.log(current(state));
// this one works.
// state.users = state.users.concat(action.payload);
});
},
});
// selector
export const selectUserById = (state, userId) =>
state.users.users.find((user) => user.id === userId);
This is the error I get (referring to selector):
TypeError: state.users.users.find is not a function
And this is my console:
// This is my state.
{
"users": []
}
// this is action.payload. Which is an array of 3 objects.
[
{
obj1
},
{
obj2
},
{
obj3
}
]
// This is the state after using push:
{
"users": 3
}
Well, such a silly mistake.
As y'all know, push method does not return anything. Thus, it made no sense for me to try to assign it to my state. concat method on the other hand, returns a new array. That's why it worked.
Here is what I changed:
state.users.push(...action.payload);
There is not state.users = anymore.
I am trying to pass values from API to state but always give this error.
TypeError: Cannot read property 'ids' of undefined
selectIds
I am using the 'reduxjs/toolkit' I try everything but still continue that error could you please help me
this is a code from the Slic file
export const getListNamesDictionary = createAsyncThunk('dictionary/names/getNames', async () => {
try {
const response = await axios.get('http://localhost:6005/api/lookup/list-name');
const data = await response.data;
// dispatch(getNames(data));
debugger;
console.log(data);
return data;
} catch (error) {
return console.error(error.message);
}
});
const namesAdapter = createEntityAdapter({});
and the Slic :
const namesDictionarySlice = createSlice({
name: 'names',
initialState: {
names: []
},
reducers: {
},
extractors: {
[getListNamesDictionary.fulfilled]: (state, action) => {
state.entities.push(action.payload);
}
}
});
export const { selectAll: selectNamesDictionary } = namesAdapter.getSelectors(state => state.data);
and this code from component where I need to dispatch the action
const names = useSelector(selectNamesDictionary);
useEffect(() => {
// dispatch(getListNamesDictionary()).then(() => setLoading(false));
dispatch(getListNamesDictionary()).then(() => setLoading(false));
}, [dispatch]);
any suggesting why that error? and thanks
You are not using the entity adapter properly. It expects to manage a state in the form:
{
ids: [1, 2],
entities: {
1: {/*...*/},
2: {/*...*/}
}
}
Your names slice doesn't match that shape. But that's an easy fix as the namesAdapter provides all of the needed tools. Quick rundown of errors to fix:
property name extractors should be extraReducers
state.entities.push needs to be replaced with an adapter function
initialState needs to have properties ids and entities
selectors need to target the correct location
const namesAdapter = createEntityAdapter({});
const namesDictionarySlice = createSlice({
name: "names",
initialState: namesAdapter.getInitialState(),
reducers: {},
extraReducers: {
[getListNamesDictionary.fulfilled]: namesAdapter.upsertMany
}
});
This fixes the first three bullets. Regarding the reducer, it might make more sense if you write it out like this, but it does the same thing.
[getListNamesDictionary.fulfilled]: (state, action) => {
namesAdapter.upsertMany(state, action)
}
The last bullet point is the cause of the specific error message the you posted:
TypeError: Cannot read property 'ids' of undefined
It actually seems like state.data is undefined. Is this namesDictionarySlice being used to control the data property of your root state? If it is something else, like state.names, then you need to change your selectors to namesAdapter.getSelectors(state => state.names).
If your store looks like this:
const store = configureStore({
reducer: {
names: namesReducer
}
});
You would want:
export const { selectAll: selectNamesDictionary } = namesAdapter.getSelectors(
(state) => state.names // select the entity adapter data from the root state
);
in Slic function, I make a mistake in writing, I most write 'extraReducer' but I wrote "extractors" :D
Upgrading meteor (from 1.4 to 1.7) and react (from 15.3.2 to 16.8.6).
"react-redux": "^4.4.10"
"redux": "3.5.2"
I found my codes were unable to update/store using Store.dispatch(), the Store just not updated.
My ACTIONS file as below:
actions/config.js
...
export default {
load({Meteor, Store}) {
return new Promise((resolve, reject) => {
Meteor.call('variables.load', null, (err, data) => {
if (err) {
reject({_error: err.reason });
return;
}
console.log("************ Store (A) = "+JSON.stringify(Store.getState()))
Store.dispatch({
type: LOAD_CONFIG,
data
});
resolve();
console.log("************ Store (B) = "+JSON.stringify(Store.getState()))
});
});
},
...
Both the console.log() were having the following:
Store (A) = {"router":{"locationBeforeTransitions":{"pathname":"/settings/config","search":"","hash":"","action":"PUSH","key":"zif4ls","basename":"/crm","query":{}}},"form":{"config":{"syncErrors":{"reportLimit":"Required"}}},"loadingBar":{}}
Store (B) = {"router":{"locationBeforeTransitions":{"pathname":"/settings/config","search":"","hash":"","action":"PUSH","key":"zif4ls","basename":"/crm","query":{}}},"form":{"config":{"syncErrors":{"reportLimit":"Required"}}},"loadingBar":{}}
Which I do expect it will have something like "reportLimit":6 , which was confirmed to have loaded into the data variable. Instead, I was getting the following error in browser console:
Uncaught TypeError: Cannot read property 'data' of undefined
Is there anything wrong/breaking changes you could think of? As these codes had been working before the upgrade.
EDIT:
I've further narrowed down the problem. It may seems to be my Routes is not calling the Reducer.
I've since change the code in my Routes to remove the need to use require.ensure .
routes.jsx (prior)
{
path: 'config',
getComponent(nextState, cb) {
require.ensure([], (require) => {
Store.injectReducer('config', require('./reducers').config)
cb(null, require('./containers/config.js'))
}, 'config')
}
},
routes.jsx (latest, to get rid of require.ensure)
{
path: 'config',
getComponent(nextState, cb) {
import('./containers/config.js')
.then(mod => {Store.injectReducer('config', require('./reducers').config);
cb(null, mod);});
}
},
Then I notice that in the reducer:
reducer/config.js
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[LOAD_CONFIG]: (state, action) => ({
...state,
data: action.data
})
};
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
data: null
};
export default function configReducer(state = initialState, action) {
console.log("************ Reducer")
const handler = ACTION_HANDLERS[action.type];
return handler ? handler(state, action) : state;
}
As per logged, function configReducer doesn't seem to have been called elsewhere.
After much trial-and-error, confirmed the problem is with the routing part, final changes:
routes.jsx (final)
{
path: 'config',
getComponent(nextState, cb) {
import('./containers/config').then(mod => {
Store.injectReducer('config', require('./reducers/config').default);
cb(null, mod.default);
});
}
}
Key points:
1) This is how the to migrate from require.ensure (used by webpack) to without relying on webpack (which was my case as am fully using Meteor Atmosphere to run)
2) mod and require(...).xxx had changed to mod.default and require(...).default if reducer function is exported as export default, otherwise said reducer will not be called.
Really took me weeks to figure this out!
Try this:
Store.dispatch({
type: LOAD_CONFIG,
data: data
});