Closed resdevd closed 5 years ago
useGraphQL()
is a React hook; it's not meant to be used in a callback like that. You pull out the load
function at the top of your component's render so it can be used on demand later:
const Foo = () => {
const { load } = useGraphQL(/* … */)
// Somewhere in a callback function call load()…
}
Alternatively, you can pull in the GraphQL
instance from context and then directly use the operate()
method on demand.
I don't use Formik, it's too bloated in relation to bundle size and as of a few weeks ago when I was looking at it, it's not using React hooks. If/when it does use hooks they will likely be in addition to the legacy APIs.
It's good to make some lightweight hooks yourself. For inspiration, here is what my forms look like:
import PropTypes from 'prop-types'
import { useField } from '../hooks/useField'
import { useWhimsyForm } from '../hooks/useWhimsyForm'
import { Fieldset } from './Fieldset'
import { Form } from './Form'
import { InputText } from './InputText'
export const UserEditName = ({ name, nameLengthMax }) => {
const {
value: valueName,
errors: errorsName,
addError: addErrorName,
onChangeInput: onChangeInputName
} = useField({ initialValue: name })
const operationName = 'userEditName'
const { formRef, message, loading, success, onSubmit } = useWhimsyForm({
operationName,
operation: {
query: /* GraphQL */ `
mutation {
${operationName}(name: "${valueName}") {
success
errors {
message
field
}
}
}
`
},
failMessage: 'Name edit failed',
onFieldError({ message, field }) {
switch (field[0]) {
case 'name':
addErrorName(message)
}
}
})
return (
<Form
loading={loading}
success={success}
message={message}
submitText="Save"
onSubmit={onSubmit}
formRef={formRef}
>
<Fieldset legend="Full name">
<InputText
placeholder="Full name"
autoComplete="name"
required
maxLength={nameLengthMax}
value={valueName}
validationMessage={errorsName.join(' ')}
onChange={onChangeInputName}
/>
</Fieldset>
</Form>
)
}
UserEditName.propTypes = {
name: PropTypes.string.isRequired,
nameLengthMax: PropTypes.number.isRequired
}
import React from 'react'
export const useField = ({ initialValue, initialErrors = [] }) => {
const [value, setStateValue] = React.useState(initialValue)
const [errors, setErrors] = React.useState(initialErrors)
const setValue = value => {
setStateValue(value)
setErrors([])
}
const addError = message => setErrors(errors => [...errors, message])
const onChangeInput = ({ target: { value } }) => setValue(value)
return { value, setValue, errors, setErrors, addError, onChangeInput }
}
import { GraphQLContext } from 'graphql-react'
import React from 'react'
import { useWhimsyAPI } from './useWhimsyAPI'
export const useWhimsyForm = ({
operationName,
operation,
reloadOnLoad = true,
failMessage,
onFieldError,
onSuccess
}) => {
const graphql = React.useContext(GraphQLContext)
const formRef = React.useRef(null)
const [attempts, setAttempts] = React.useState(0)
const [success, setSuccess] = React.useState(null)
const [message, setMessage] = React.useState(null)
const { load, loading, cacheKey } = useWhimsyAPI({
loadOnMount: false,
loadOnReload: false,
loadOnReset: false,
reloadOnLoad,
operation
})
const onGraphQLCache = React.useCallback(
({
cacheKey: cachedCacheKey,
cacheValue: { data: { [operationName]: payload = {} } = {} } = {}
}) => {
if (cacheKey === cachedCacheKey) {
setSuccess(payload.success)
if (payload.errors) {
const formErrors = []
payload.errors.forEach(error => {
if (error.field) {
if (onFieldError) onFieldError(error)
} else formErrors.push(error.message)
})
setMessage(
`${failMessage}${
formErrors.length ? `: ${formErrors.join(' ')}` : '.'
}`
)
} else if (payload.success) {
if (onSuccess) onSuccess(payload)
} else setMessage(`${failMessage}.`)
}
},
[cacheKey, failMessage, onFieldError, onSuccess, operationName]
)
const onSubmit = () => {
setSuccess(null)
setMessage(null)
setAttempts(attempts => attempts + 1)
load()
}
React.useEffect(() => {
graphql.on('cache', onGraphQLCache)
return () => graphql.off('cache', onGraphQLCache)
}, [graphql, onGraphQLCache])
React.useEffect(() => {
if (attempts) {
const timeout = setTimeout(() => {
if (formRef.current) formRef.current.reportValidity()
}, 200)
return () => clearTimeout(timeout)
}
}, [formRef, attempts])
return { formRef, loading, success, message, setMessage, onSubmit }
}
import { useGraphQL } from 'graphql-react'
import React from 'react'
import { whimsyFetchOptionsOverride } from '../api'
import { CookieContext } from '../components/CookieContext'
export const useWhimsyAPI = options => {
const getCookie = React.useContext(CookieContext)
return useGraphQL({
fetchOptionsOverride: whimsyFetchOptionsOverride(getCookie),
...options
})
}
You could still use Formik, but you would have to use the useGraphQL()
hook properly and work it out yourself 🤓
I just tweaked the above example to fix a few bugs, you can see the diff via the edits menu.
@jaydenseric Would the ability to pass new options when calling load()
be something you could see being part of your library? Using Formik or not, I think it's a pretty common case not to know the values of the variables your mutation requires when calling useGraphQL()
in the body of your functional component. Being able to provide those in a callback would be a great.
Loads the GraphQL operation on demand, updating the GraphQL cache.
Parameter | Type | Description |
---|---|---|
options |
Object | Options. |
options.fetchOptionsOverride |
GraphQLFetchOptionsOverride? | Overrides default fetch options for the GraphQL operation. |
options.operation |
GraphQLOperation | GraphQL operation. |
export const AuthProvider = ({ children }) => {
const { load } = useGraphQL({
/* ... */
operation: {
query: /* GraphQL */ `
mutation($input: SignInInput!) {
signIn(input: $input) {
token
}
}
`,
// Note we don't provide variables at this time as they are not known.
},
});
const signIn = async (username, password) => {
await load({
operation: {
variables: {
input: {
username,
password,
},
},
},
});
/* ... */
};
return (
<AuthContext.Provider value={{ signIn }}>{children}</AuthContext.Provider>
);
};
@olistic the useGraphQL
hook needs the variables and everything to be fed into it for it to be able to tell the loading status and cache of that exact query/mutation. This is pretty straightforward if you track the input state via hooks before the useGraphQL
hook, in the same component.
As mentioned earlier, you can pull in the GraphQL
instance from context and then directly use the operate()
method on demand.
My bad if the questions comes off as ignorant. Coming from data engineering background, I'm still a beginner in web development.
I've using UseGraphQL hook to fetch the data successfully and able to display the data on a nextjs ssr website. However when creating a form page with Formik library, I'm stuck at the error message "Hooks can only be called inside the body of a function component." Cannot find any similar examples using form to mutate data using GraphQL hook anywhere.
_app.js File:
FormPage File:
This results to "Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)" error.
You might have mismatching versions of React and React DOM. (Ruled out) You might be breaking the Rules of Hooks. (Assuming this is causing error) You might have more than one copy of React in the same app. (Ruled out)
Can anyone point me towards right direction? Under assumption MutateContact is a function in the above code, so usage of hooks should be okay right?