Thursday, June 9, 2022

Jotai State + React Hooks === React Native easy state system

 We moved our local state management recently to Jotai.   It has greatly simplified state mangement for our React Native app.   Now the hooks act as the complete state manager, so that the UI portion doesn't need to worry about it.    

I won't give detailed info about Jotai here, as you can look at other tutorials/docs for that.   But serveral key points:

  • App wide state is very easy and straightforward with jotai atoms
  • Backing the jotai atom with AsyncStorage means state can automatically be saved/rehydrated between app runs
  • Business logic can be easily automated in the hook, so it is reused throughout the app
  • using derived jotai atoms means your calculated states are always in sync with other state changes


Here's an example from a useCustomer hook.   Name and phone are easily stored and will stay even if I shut the app down and bring it back up,  without writing any code on app startup!

import AsyncStorage from '@react-native-community/async-storage';
import {atom, useAtom} from 'jotai';
import {atomWithStorage} from 'jotai/utils';

import {
formatPhoneDisplayWithParens,
formatOnlyNumber,
NAME_REGEX,
} from '../utils/text';

const ATOM_STORAGE = {
...AsyncStorage,
delayInit: true,
};

const firstNameAtom = atomWithStorage('firstName', '', ATOM_STORAGE);
const lastNameAtom = atomWithStorage('lastName', '', ATOM_STORAGE);
const phoneAtom = atomWithStorage('phone', '', ATOM_STORAGE);

// Automatically creates a display version of the phone number every
// time the phone atom is changed, but ONLY when it changes
const phoneDisplayAtom = atom(get =>
formatPhoneDisplayWithParens(get(phoneAtom)),
);



function useCustomer() {
const [firstName, setFirstNameValue] = useAtom(firstNameAtom);
const [lastName, setLastNameValue] = useAtom(lastNameAtom);
const [phone, setPhoneValue] = useAtom(phoneAtom);

const [phoneDisplay] = useAtom(phoneDisplayAtom);

// Automatically handles limiting name to certain characters
const setFirstName = value => {
const newValue = value.match(NAME_REGEX) ? value : firstName;
setFirstNameValue(newValue);
};

// Automatically handles limiting name to certain characters
const setLastName = value => {
const newValue = value.match(NAME_REGEX) ? value : lastName;
setLastNameValue(newValue);
};

const setPhone = value => {
const newValue = formatOnlyNumber(value);
if (newValue?.length > 10) {
return;
}
setPhoneValue(newValue);
};

const hasCustomerData = () => {
let valid = false;

firstName && (valid = true);
lastName && (valid = true);
phone && (valid = true);

return valid;
};

return {
firstName,
lastName,
phone,
phoneDisplay,
setFirstName,
setLastName,
setPhone,
hasCustomerData,
};
}

export default useCustomer;

Friday, January 22, 2021

React Re-architecture with NextJS, Apollo GQL, React Hooks and Styled Components

 We have a business critical web app running on React, with a Redux state management and restful services.  It's been in production for years now and is stable, but since then we have learned a lot and brought in new technologies.

NextJS 10

What a major change and simplification of the architecture.   No more setting up WebPack,  or React Router, or much of anything really.   Automatic hot reloading in dev mode, plus a built-in production build mode makes project setup easy.   You can have a project setup and ready deploy to prod in minutes.... literally!   I won't do a full explanation of all the features of NextJS, but just a quick summary of a couple of my favorites.

Routing

To create a new page/route, just put a javascript file in the pages directory with the name of the page.  So /pages/status.js will let you be able to hit http://localhost:3000/status and display that page.   Your homepage is simply /pages/index.js.     To make a link, you just use the built in Link react component.  Or you can use the router object to push to a new route.
router.push('/status')

One other fancy and very useful thing is built in url param handling.   So say you want to put a customer number in your url you could make a file /pages/customer/[customerId].js and when it is called like this http://localhost:3000/customer/8675309 then router.query will automatically have customerId=8675309 in it.   No more need for custom servers.  It's automatic!

Static Optimization

As long as you follow the rules, NextJS 10 will automatically statically optimize your pages and write them out as html.   So they will load very fast.   Go one step further and do a next export and your whole size becomes a deployable web page that you can deploy on any http server (S3 for example).  We will take down 12 clusters of servers as we will be able to deploy them on S3 instead of standing up a web server cluster for each app and all 4 environments (dev/qa/stg/prd).

Styled Components

There are many good css in js libraries with similar features but the reason we chose styled component was that it also had good support in React Native as well as React.  This has paid off as we have some of the exact same screens in our mobile app as we do in our web app.   They ported over with minor changes!

One thing we love is that careful naming can make your code much more readable.
<div className="header">
<span className="label">Name</span>
<span className="value">Value</span>
</div>

Becomes this (ignore the blank space.... can't get it out of there):

<Header>
<Label>Name</Label>
<Value className="value">Value</Value>

</Header> 

 This becomes even more apparent on a busy page where you'd normally have 3 - 4 layers of nested divs, but instead in styled components each has a unique name like <Page>, <Detail>, <Summary> etc. so it's easy to tell where each part begins and ends.

You can also easily extend a styled component.   So if I made a <GridValue> with 20 properties set and the text was centered and I wanted a <NumberGridValue> with a text align of right, you just extend <GridValue> and change the one property you need to change but inherit all the rest.

const NumberGridValue = styled(GridValue)

Another plus is that it uses all standard css.   So if you are converting or have a designer who mocks pages for you you can copy/paste it into a styled component as is.

One final hurrah is that you can send properties into a styled component and make the rendering change based on the property.

color: ${props => props.isSelected ? red : blue};


Apollo GraphQL

We converted our REST endpoints to GraphQL queries and mutations.  Many features led us to GQL with the built in yet simple typing, automatic input checking, automatic caching, local state support, simple client-side querying and very powerful hooks in React.


One really big advantage is being able to store local client state in GQL and the ability to query it out using the same syntax as the server side queries.  So say the user makes a selection in one part of the app and we write it to local state.   Everywhere else in the app that queries that same state will automatically be updated.  Change your selected store in the store finder, and it automatically updates in the header!


GQL lets you query out only the fields you need.   So say you're getting store detail, but in this case you only need the name to display.   You can query just the name and not get the other 50 fields.  Also if you've already queried all 50 fields (say for the store locator) your query for the store name doesn't have to hit the server again because GQL is smart enough to get it from cache.  You can also run multiple queries at once.   So when we get product attributes and product inventory (separate queries) we can run them both at once which reduces calls to the server.

Another major win was the ability to have local state drive the next query in a series of steps.    So we used queries that were conditional on local state items, so they would skip the query until that item was set.   So it happens like this:

  1.  step1 value set => step 2 query runs automatically
  2.  step2 value set => step 3 query runs automatically

We had a 6 step process and before we had large switch statements and tons of step handlers passed around to make sure all the steps happen at all the right times.  Now it happens automatically by setting the state.   No matter who sets the state, it will run or re-run if state changes.   So click on the header and change a value..... query re-runs.   Go through the wizard and change the value.... query re-runs.

For example this is one of our hooks and automatically gets product info and inventory once a siteNumber is chosen and a list of articles comes back from another query.


const {loading} = useQuery(GET_PRODUCT_ATTRIBUTES, {
skip: !articles.length > 0 || !siteNumber,
variables: {
input: {site: siteNumber, articles},
},

onCompleted(data) {
client.writeQuery({
query: STATE_QUERY,
data: {
productAttributes: data.productAttributes,
productInventory: data.productInventory,
},
});
},
onError(error) {
client.writeQuery({
query: STATE_QUERY,
data: {
error,
},
});
},
});


We also took advantage of our local state tree and save the errors there.   So we have one component on our app that hovers over the page to show errors.   Write null to the error in state and it goes away.  Write the error on ANY query and it shows!  We even re-used the same error component on our login page to show errors there without any extra coding.... just plopped it on the page.


React Hooks

 We made extensive use of hooks to orchestrate complex operations and encapsulate a lot of business/query logic.   So as you saw above, the query for product attributes was in a hook that was triggered when other state was set.   The results get saved into state as well, so become available to any component by looking in the state.  Have a problem with the product query?  You only have to look in one place regardless of which component is showing the results...... product list, product comparison, product detail all use the same data written from the same hook.

If it was complex enough, we'd use one hook to orchestrate and call other hooks that did subtasks of the big task.  We made each hook do one thing and do it well so the code was small, concise and easy to understand.   Because all our data was in GQL local state, the hooks could trigger off that and no data had to be passed back and forth between hooks.   So they were related, but not tightly coupled.

Conclusion

We've learned a lot over the last 5 years.   Leveraging all the new technologies we've learned has resulted in app that:
  • has less lines of code (30% less or so)
  • is more stable
  • is easier to maintain
  • is much faster!


Tuesday, July 28, 2020

React Router Native AnimatedSwitch

I wanted to automate the animations in my react native apps. Since I was using React Router I read up on their blurb on animating transitions. I came up with an AnimatedSwitch functional component for animating route changes.

So you use the component something like this:

<AnimatedSwitch {...this.props} exact>
<Route exact path="/" component={Home} />
<Route path="/customer" component={CustomerPage} />
<Route path="/about" component={AboutPage} />
</AnimatedSwitch>


If going to a new route, by default it will slide in from the right.   If you are going back or popping a route or even replacing a route, by default it will slide in from the left.

You can also change to a fade animation by changing the animationType to ANIMATE_FADE instead of the default of ANIMATE_SLIDE.   

You can also make a custom animationType by matching the shape of the 2 predefined animations.  For that object:
  • prev is the animation for the outgoing route
  • new is the animation for the incoming route
  • backPrevious is the animation for the outgoing route when going back or popping off the history
  • backNew is the animation for the incoming route when going back or popping off the history

In the animationSetup variable you can pass in a custom animation object to override the values used for the animation, such as type (decay, timing, spring) and then the setup variables for that type of animation directly matching the Animated API in React Native.

I hope you find it useful.   With a bit of work, it could also be modified to work with the web version of react router by switching out the Animated.Views with divs and css animations.    I haven't done that as we use NextJS for our web apps.

This is the usePrevious hook used to keep track of location and children of the previous render.   It needs to be available to AnimatedSwitch (you may need to adjust the import if your file structure doesn't match mine).   

import {useEffect, useRef} from 'react';

/**
* Any easy way to store the last value of a variable
* (maybe one set from useState).
*
* This can replace prevProps from componentDidUpdate.
*
* @param value The value to store.
*/
function usePrevious(value) {
const ref = useRef();

// Update the value in ref AFTER the render
// ready for the next call
useEffect(() => {
ref.current = value;
});

// This returns the value before the useEffect hook
// fires, so it is still the previous value
return ref.current;
}

export default usePrevious;

This is the AnimatedSwitch component.

import React, {useState, useEffect} from 'react';
import {Animated, Dimensions, View} from 'react-native';
import {
Switch,
matchPath,
useLocation,
useHistory,
Route,
} from 'react-router-native';

import usePrevious from '../../hooks/usePrevious';

const {width} = Dimensions.get('window');

export const DEFAULT_ANIMATION_SETUP = {
type: 'timing',
init: {
fromValue: 0,
toValue: 1,
duration: 250,
useNativeDriver: true,
},
};

export const ANIMATE_SLIDE = {
previous: anim => ({
position: 'absolute',
transform: [
{
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [0, -width],
}),
},
],
}),
new: anim => ({
position: 'absolute',
transform: [
{
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [width, 0],
}),
},
],
}),
backPrevious: anim => ({
position: 'absolute',
transform: [
{
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [0, width],
}),
},
],
}),
backNew: anim => ({
position: 'absolute',
transform: [
{
translateX: anim.interpolate({
inputRange: [0, 1],
outputRange: [-width, 0],
}),
},
],
}),
};

export const ANIMATE_FADE = {
previous: anim => ({
opacity: anim.interpolate({
inputRange: [0, 0.75],
outputRange: [1, 0],
}),
}),
new: anim => ({
opacity: anim.interpolate({
inputRange: [0, 0.75],
outputRange: [0, 1],
}),
}),
backPrevious: anim => ({
opacity: anim.interpolate({
inputRange: [0, 0.75],
outputRange: [1, 0],
}),
}),
backNew: anim => ({
opacity: anim.interpolate({
inputRange: [0, 0.75],
outputRange: [0, 1],
}),
}),
};

const getPreviousRoute = (exact, prevPath, routes) => {
if (!routes) {
return null;
}
return routes.find(route => {
const match = matchPath(prevPath.pathname, route.props);

if (exact) {
return match != null && match.isExact;
} else {
return match != null;
}
});
};

const renderPreviousRoute = previousRoute => {
if (previousRoute) {
return previousRoute.props.component ? (
React.createElement(previousRoute.props.component)
) : (
<Route render={previousRoute.props.render} />
);
} else {
return null;
}
};

function AnimatedSwitch({
children,
exact,
animationType = ANIMATE_SLIDE,
animationSetup = DEFAULT_ANIMATION_SETUP,
}) {
const [animating, setAnimating] = useState(false);
const [anim] = useState(new Animated.Value(0));
const [previousRoute, setPreviousRoute] = useState();

const location = useLocation();
const history = useHistory();

// we're going to save the previous matching route so we can render
// it when it doesn't actually match the location anymore
const previousLocation = usePrevious(location);
const previousChildren = usePrevious(children);

// now save the animation type for both previous and new views
const [newAnimationStyle, setNewAnimationStyle] = useState();
const [prevAnimationStyle, setPrevAnimationStyle] = useState();

const finishAnimating = () => {
setAnimating(false);
setPreviousRoute();
};

const needsAnimation = previousLocation !== location;

useEffect(() => {
const newPreviousRoute = getPreviousRoute(
exact,
previousLocation,
previousChildren,
);

if (needsAnimation && newPreviousRoute) {
// we were rendering, but now we're heading back up to the parent,
// so we need to save the newPreviousRoute so we can render it
// while the animation is playing

if (history.action === 'POP' || history.action === 'REPLACE') {
setNewAnimationStyle(animationType.backNew(anim));
setPrevAnimationStyle(animationType.backPrevious(anim));
} else {
setNewAnimationStyle(animationType.new(anim));
setPrevAnimationStyle(animationType.previous(anim));
}

setPreviousRoute(newPreviousRoute);

setAnimating(true);
}
}, [
location,
needsAnimation,
animationType,
previousChildren,
previousLocation,
history.action,
anim,
exact,
]);

useEffect(() => {
if (animating) {
switch (animationSetup.type) {
case 'decay':
Animated.decay(anim, animationSetup.init).start(finishAnimating);
break;
case 'timing':
Animated.timing(anim, animationSetup.init).start(finishAnimating);
break;
case 'spring':
default:
Animated.spring(anim, animationSetup.init).start(finishAnimating);
}
}
}, [animating, anim, animationSetup]);

// Need to render the previous route for the time between not animating and animating
if (needsAnimation && !animating) {
const tempPreviousRoute = getPreviousRoute(
exact,
previousLocation,
previousChildren,
);

if (tempPreviousRoute) {
const prevRouteComp = renderPreviousRoute(tempPreviousRoute);
return prevRouteComp;
} else {
return null;
}
}

if (animating) {
// Animate both the previous route and the new route at the same time,
// then change them to the new route based on the animation type, so either slide or fade or spring
const prevRouteComp = renderPreviousRoute(previousRoute);
return (
<View>
<Animated.View key="newView" style={newAnimationStyle}>
<Switch>{children}</Switch>
</Animated.View>

<Animated.View key="prevView" style={prevAnimationStyle}>
<Switch>{prevRouteComp}</Switch>
</Animated.View>
</View>
);
} else {
// Just animate the actual route from the location
return (
<View>
<Animated.View key="newView">
<Switch>{children}</Switch>
</Animated.View>
</View>
);
}
}

export default AnimatedSwitch;

Tuesday, March 27, 2018

Running Expo offline

Offline Expo:

I wanted to continue working on my expo project on my laptop while no wifi was present.   It was very hard to find information on how to do get it running, or if it was even possible.

Finally found the 2 magic commands!

This command will start the offline server.   No login required.
exp start --offline

This command will start the app in a running android emulator.
exp android --offline


To make this even easier (and so I didn't have to remember the commands) I just added them to my scripts in package.json

"scripts": {
"server": "exp start --offline",
"emulator": "exp android --offline"
}


Then I just run on one terminal:
npm run emulator


And in another terminal:
npm run server


Documentation:
To get the full offline experience, I also downloaded react-native, react-native-elements and expo from github so I can access the docs offline!   The gh-pages branch in react-native and react-native-elements contain the doc files, though the links won't work, so you have to browse to them manually.   Still better than no docs at all!

Thursday, March 22, 2018

Expo Icon Fonts with React Native and React Native Elements!

I was trying to use the Avatar and Icon objects from react-native-elements I kept getting the following error:
fontFamily 'MaterialIcons' is not a system font and has not been loaded through Expo.Font.loadAsync.

- If you intended to use a system font, make sure you typed the name correctly and that it is supported by your device operating system.

- If this is a custom font, be sure to load it with Expo.Font.loadAsync.

It was driving me nuts.   Googling for the answer just brought up snippets of information about what to do.   So finally I pieced together the parts to get it working.

You have to load the fonts before they are used.   It seems that if you ever blow away your node_modules and then do npm install again, you lose the built in loading.  So you have to do it manually.  Here is how!

I made sure the @expo/vector icons are loaded:

npm install --save @expo/vector-icons


Then I changed the App to load them directly:

import React from 'react';
import { View } from 'react-native';
import { Avatar } from 'react-native-elements';
import { AppLoading, Font } from 'expo';

import FontAwesome  
from './node_modules/@expo/vector-icons/fonts/FontAwesome.ttf';
import MaterialIcons  
from './node_modules/@expo/vector-icons/fonts/MaterialIcons.ttf';
export default class App extends React.Component {
state = {
fontLoaded: false
};

async componentWillMount() {
try {
await Font.loadAsync({
FontAwesome,
MaterialIcons
});
this.setState({ fontLoaded: true });
} catch (error) {
console.log('error loading icon fonts', error);
}
}
render() {
if (!this.state.fontLoaded) {
return <AppLoading />;
}

return (
<View>
<Text>My App</Text>
<Avatar
small
rounded
icon={{ name: 'add' }}
/>
</View>
);
}
}

So now the fonts load before the app is shown.   While they are loading, the AppLoading continues to render the loading screen before showing any of the app.   The fonts get loaded, then the state is set so the AppLoading component no longer renders and it continues to your app.

But why throw all that into the main App.js?    It's messy.   So I made an AppFontLoader utility that looks like this:


import React from 'react';
import { AppLoading, Font } from 'expo';

import FontAwesome 
from '../../node_modules/@expo/vector-icons/fonts/FontAwesome.ttf';
import MaterialIcons  
from '../../node_modules/@expo/vector-icons/fonts/MaterialIcons.ttf';

class AppFontLoader extends React.Component {
state = {
fontLoaded: false
};

async componentWillMount() {
try {
await Font.loadAsync({
FontAwesome,
MaterialIcons
});
this.setState({ fontLoaded: true });
} catch (error) {
console.log('error loading icon fonts', error);
}
}

render() {
if (!this.state.fontLoaded) {
return <AppLoading />;
}

return this.props.children;
}
}

export { AppFontLoader };

Now the App.js gets simplified!

import React from 'react';
import { View } from 'react-native';
import { Avatar } from 'react-native-elements';
import { AppFontLoader } from './src/utils';
export default class App extends React.Component { render() {
return ( 
<AppFontLoader>
 <View>
<Text>My App</Text>
<Avatar
small
rounded
icon={{ name: 'add' }}
/>
</View>
</AppFontLoader>
);
}
}

I hope this helps you.   It shouldn't take 4 hours to figure this out!

Friday, September 29, 2017

Javascript/React project template

After doing our first big React SPA (Single Page Application) I decided to take all the project setup out and make a template project from it.   So for future projects we just unzip the project into our repository and we can start programming.

Github React Template

Here's the synopsis from the README.md

react-redux-template

Base template for enterprise react-redux projects with feature based layout. Includes setup for the following:
  • React
  • Redux with sagas
  • React-Router v4
  • SASS CSS processing
    • Global variables
    • File per component layout
  • Feature based layout - directory for each feature
    • Container
    • Styles
    • Actions & Constants
    • Reducer
    • Saga
  • Express Server
    • Public folder for security
    • Route controllers
    • HTTPS with default key
    • Async/Await syntax
    • Winston logging
      • Log level modification service
      • Logging setup parameters including automatic file rotation/deletion
    • Multi-threading
    • Hot reloading client code in dev mode
    • Hot reloading server code in dev mode
  • Production ready webpack with compression and latest javascript syntax

Wednesday, April 12, 2017

AWS config getCredentials as a promise

Getting AWS to work with async/await took some doing.   Finally have a nice little library using 
aws-sdk and aws-api-gateway-client.


// AWS API Gateway Setup - Test Inventory Callconst AWS = require('aws-sdk');
const apigClientFactory = require('aws-api-gateway-client');

const { INVENTORYLOOKUP } = appSettings.service_endpoints;

async function getAwsConfig() {
    return new Promise(function (resolve, reject) {
        AWS.config.getCredentials(async function(err) {
            if (err) {
                console.log('Error getting credentials', err);
                return reject(err);
            } else {
                resolve({
                    accessKey: AWS.config.credentials.accessKeyId,
                    secretKey: AWS.config.credentials.secretAccessKey,
                    sessionToken: AWS.config.credentials.sessionToken,
                    region: 'us-west-2'                });
            }
        });
    });
}

async function invokeAWS(targetURL, body) {

        let config = await getAwsConfig();
        config.invokeUrl = targetURL;

        console.log('config=', config);

        const apigClient = apigClientFactory.newClient(config);

        // config, url, method, header, body        let prom = apigClient.invokeApi({}, '/', 'POST', {
            'Content-Type': 'application/json',
            'Accept': 'application/json'        }, body);

        return prom;
    }
}