Skip to content

Instantly share code, notes, and snippets.

@mfbx9da4
Last active May 11, 2019 22:23
Show Gist options
  • Save mfbx9da4/9035caaabe9ea95d7a53d21154606d30 to your computer and use it in GitHub Desktop.
Save mfbx9da4/9035caaabe9ea95d7a53d21154606d30 to your computer and use it in GitHub Desktop.
A first draft of a re-implementation of Formik using react hooks specialised for react native
import React, {
useState,
SetStateAction,
createContext,
Dispatch,
ReactNode
} from 'react'
import { TextInput, TextInputProps } from 'react-native'
export interface FormContextI<T> {
values: T
errors: { [key in keyof T | string]?: string }
setValues: Dispatch<SetStateAction<T>>
handleChange: (key: keyof T) => (value: string) => void
handleBlur: (key: keyof T) => () => void
handleSubmit: () => void
setSubmitting: (value: boolean) => void
setFieldError: (field: string, message: string) => void
isSubmitting: boolean
isValidating: boolean
}
export interface FormConfigI<T extends { [key: string]: string }> {
initialValues: T
onSubmit: (values: T) => void
}
function shallowCloneWithDefaults<
T extends { [key: string]: unknown },
D extends unknown
>(values: T, defaultValue: D) {
type U = { [K in keyof T]: D }
const keys = Object.keys(values) as Array<keyof U>
const partial: Partial<U> = {}
for (let k in keys) {
partial[k] = defaultValue
}
const final = partial as U
return final
}
function useForm<T extends { [key: string]: string }>({
initialValues,
onSubmit
}: FormConfigI<T>): FormContextI<T> {
type Key = keyof typeof initialValues
const [values, setValues] = useState(initialValues)
const [isSubmitting, setIsSubmitting] = useState(false)
const [isValidating, setValidating] = useState(false)
const initialTouched = shallowCloneWithDefaults(values, false)
const [touched, setTouched] = useState(initialTouched)
const initialErrors: FormContextI<T>['errors'] = {}
const [errors, setErrors] = useState(initialErrors)
const handleBlur = (field: Key) => () => {
setTouched({ ...touched, [field]: true })
}
const handleChange = (field: Key) => (value: string) => {
setValues({ ...values, [field]: value })
}
const setFieldError = (field: string, message: string) => {
if (errors[field] === message) return
setErrors({ ...errors, [field]: message })
}
const setSubmitting = (value: boolean) => {
if (isSubmitting === value) return
setIsSubmitting(value)
}
async function handleSubmit() {
onSubmit(values)
}
return {
values,
setValues,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
isValidating,
setSubmitting,
setFieldError,
errors
}
}
export function TextInputField<T extends { [key: string]: string }>({
name,
context,
...rest
}: { name: keyof T } & TextInputProps & { context: FormContextI<T> }) {
return (
<TextInput
{...rest}
value={context.values[name]}
onChangeText={text => {
context.handleChange(name)(text)
rest.onChangeText && rest.onChangeText(text)
}}
onBlur={e => context.handleBlur(name)()}
/>
)
}
const { Provider: FormProvider } = createContext<FormContextI<any>>({} as any)
function Formik<T extends { [key: string]: string }>({
render,
...config
}: FormConfigI<T> & { render: (context: FormContextI<T>) => ReactNode }) {
const context = useForm<T>(config)
return <FormProvider value={context}>{render(context)}</FormProvider>
}
export default Formik
import React from 'react'
import View from 'components/View'
import gql from 'graphql-tag'
import Text from 'components/Text'
import {
StyleSheet,
ScrollView,
TextInputProps,
TouchableOpacity,
TextInput
} from 'react-native'
import { useQuery, useMutation } from 'react-apollo-hooks'
import Formik from './FormikHooks'
const SIGNIN_MUTATION = gql`
mutation SIGNIN_MUTATION($email: String!, $password: String!) {
login(email: $email, password: $password) {
id
email
name
}
}
`
interface LoginData {
id: string
email: string
name: string
}
interface LoginVariables {
[key: string]: string
email: string
password: string
}
function loginMutation() {
return useMutation<LoginData, LoginVariables>(SIGNIN_MUTATION)
}
function LoginForm() {
const initialValues: LoginVariables = {
password: 'test',
email: '[email protected]'
}
const [login, { data, error, loading }] = loginMutation()
const config = {
initialValues,
onSubmit: async (values: LoginVariables) => {
try {
await login({ variables: values })
} catch (err) {
console.log('caught error')
}
}
}
return (
<Formik
{...config}
render={context => {
const {
values,
handleChange,
handleBlur,
handleSubmit,
setFieldError,
setSubmitting,
errors,
isSubmitting
} = context
if (error) {
setFieldError(
'form',
error.graphQLErrors.map(x => x.message).join('')
)
}
setSubmitting(loading)
return (
<View p={'large'} dbg>
<View>
{isSubmitting && <Text>Loading</Text>}
{errors.form && <Text>{`${errors.form}`}</Text>}
{data && <Text>{data && JSON.stringify(data)}</Text>}
</View>
<View p={'large'} dbg>
<TextInput
placeholder="Email"
onChangeText={handleChange('email')}
onBlur={handleBlur('email')}
value={values.email}
/>
</View>
{errors.email && (
<View>
<Text style={styles.formLabel}>{errors.email}</Text>
</View>
)}
<View p={'large'} dbg>
<TextInput
value={values.password}
textContentType={'password'}
secureTextEntry
onChangeText={handleChange('password')}
onBlur={handleBlur('password')}
placeholder={'Password'}
/>
</View>
{errors.password && (
<View>
<Text style={styles.formLabel}>{errors.password}</Text>
</View>
)}
<TouchableOpacity onPress={handleSubmit}>
<View p={'large'} dbg>
<Text style={styles.formLabel}>Login</Text>
</View>
</TouchableOpacity>
</View>
)
}}
/>
)
}
function LoginScreen() {
return <ScrollView>{LoginForm()}</ScrollView>
}
const styles = StyleSheet.create({
formLabel: {
fontSize: 20,
textAlign: 'center',
margin: 10
}
})
export default LoginScreen
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment