Category Archives: Frontend

IOS Safari showing a blank page when coming back to a React Component

Problem

When you go to a react component, then go to another, and finally come back, the react component is showing blank, or half-blank.

Investigation

If you inspect the page with the mobile safari inspector , you can see the elements are there, but safari just refuse to show the data on the screen.

In my case, this seems to be related with data reloading when I come back to the component. It reloads the data in its componentDidAmount() then re-render. Safari may not perform well enough to handle this kind of re-rendering.

Fix

A workaround is to not reload the data when you come back to the component. You can do this by checking if the data exists, and only loads the data if not existing.

It comes with a price: when you come back to the component, you are not seeing the data that’s up-to-data. There are a few things you can do to make it up:

  • A delayed data re-loading
  • Always reload the data 24 hours (e.g.) after last data loading
  • Very important: destroy the data if a user logs out to make sure a different user won’t see the first user’s data

Change the font family globally in Material-UI 3.x

Some say you should use the jsx-global-plugin like “@global body{fontFamily:’someFont’}” , but it doesn’t work in my project. The global CSS still has the lower CSS speciality than MUI components’ CSS because this global CSS is using a type selector.

You should use a theme instead

//create a theme 

const myTheme = createMuiTheme({
    typography: {
        fontFamily: "Roboto Condensed" 
    }
}


// use the theme

class MyComponent extends React.Component<Props> {

	... 
    render() {
        
            return (
                    <MuiThemeProvider theme={this.props.theme}>
                        <CssBaseline/>
                        <p>Some content</p>
                    </MuiThemeProvider>
                );
        
    }
}



Redux + Typescript: Use class to define Actions

You want the actions to be typed. You should use interface or class.

Class is better than interface because in interfaces you cannot define the action "type" as a constant, but in classes you can:

class LoadingStartedAction implements Action{
    readonly type = LOADING_STARTED
}

However a class instance is not a plain object, but Redux requires plain objects to be dispatched.

This can be handled by a middleware which converts the class instances to plain objects

// See https://github.com/reduxjs/redux/issues/992#issuecomment-167964652
const typedActionToPlain = (store: any) => (next: any) => (action: any) =>  {
    next(Object.assign({}, action));
};

export default typedActionToPlain;

Pay attention not to step into the toe of Thunk middleware when registering it:

const rootStore = createStore(rootReducer,
    applyMiddleware(thunkMiddleware,
        typedActionToPlain //this must be after thunk, otherwise it will try to convert an async function to a plain object
    )
);

Redux + Typescript: The type of the root state

You can get the type of the state from the root reducer

// the reducer
const appReducer = combineReducers({
    global: globalStateReducer,
    example: exampleReducer
});

type AppState = ReturnType<typeof appReducer>

However the redux state can be cleared in some scenarios, so it should be undefinable

//See https://stackoverflow.com/a/35641992/301447 for "undefined redux state" 
type RootState = AppState | undefined;


const rootReducer = (state:RootState, action: Action) => {
    if (action.type === DESTROY_REDUX_STATE) {
        state = undefined;
    }
    return appReducer(state, action)
}

And mapStateToProps() of a component will look like this:

function mapStateToProps(rootState: RootState) {
    return {
        example: rootState!.example  //note the exclamation mark as rootState can be undefined 
    };
}

React Component: A pair of componentDidMount() and componentWillUnmount() may cause infinite loop if error is thrown from render

The following will cause an infinite loop. The render() method will be called again and again.

class ToDoItemEditComponentContainer extends React.Component {
    componentDidMount() {        
        this.props.loadItem();  //load item remotely and put it in the local redux store      
    }

    componentWillUnmount() {
        this.props.clearItemLocally(); //remove item from local redux store
    }

    render() {
        throw "some error" 
    }
}

Here is why: "As of React 16, errors that were not caught by any error boundary will result in unmounting of the whole React component tree. " (See here). To be more specific,

  • render() throws an error
  • The react component tree is unmounted
  • componentWillUnmount() is called. And something is cleared from the reduxt store or react state
  • The component tree will be rendered again since some state has been changed

To fix: Implement componentDidCatch() hook in this component or create a global error boundary as shown in here . When there is a render() error, do not call the render() method any more, or do not change any local data in componentWillUnmount()

My practice of fetching data for List/Detail components in React/Redux

The user experience it should have

  • When you go to a detail page from the list page or with a direct URL, the detail page should show the up-to-date data
  • When you go to the list page with a direct URL, the list page should show the up-to-date data
  • When you RETURN to the list page from a detail page, the list page doesn’t have to show the up-to-date data.
  • When you RETURN to the list page from a editing page after MAKING CHANGES, the list page should show the up-to-date data to reflect the changes

Solution

  • On the list page
    • When entering the component, only fetch the data if the current data (this.props.listData) is null
    • When leaving the component, don’t clear the data fetched from Redux store. They will be needed when returned from detail pages
  • On a detail page
    • When entering the component, always fetch the data from the backend
    • When leaving the component, always clear the data locally from the redux store, otherwise the data of this item may be shown when you visit the detail page of another item. That’s because data fetching is called after render() .
    • When returning to the list page from here without any change, do nothing about the list in the redux store. And the link of returning should be an SPA link, which doesn’t force a new document creation
    • After data changing, dispatch an action to refresh the list data in the redux store. So when you go back to the list page, the data has been changed to reflect the changes

Code implementation

//List
class ListComponentContainer extends React.Component {
    componentDidMount() {
        if (this.props.itemList == null) {  
            this.props.dispatch(getItemsAsyncAction(this.props.token));
        }
    }

    render() {
        if (this.props.itemList) {
            return <ListComponent {...this.props}>;
        } else {
            return null;
        }
    }
}

//Detail
class ItemEditComponentContainer extends React.Component {
    componentDidMount() {
       dispatch(getCurrentItemAsyncAction(itemId));
    }

    componentWillUnmount() {
        dispatch(clearCurrentItemLocallyAction());
    }

    render() {
            if (this.props.currentItem) {
                return <ItemEditComponent  currentitem={this.props.currentItem}/>;
            } else {
                return null;
            }
    }
}

//and in the method of handling changes
    onSubmit() {
        dispatch(updateItemAsyncAction());
        dispatch(clearItemListLocallyAction());  //1.  getItemsAsyncAction() will also work.  2. This can also be put in the success callback of updateItemAsyncAction()
    }

What happens to the redux store if I refresh the browser?

It will go away, and then be initialised.

If you have this question, it means you have no exact idea of the lifespan of client-side state in a browser.

First of all, a document is created under current tab of the browser when a url is first requested from the server side. The client-side state’s lifespan is dependent on the lifespan of the document.

If your website is not an SPA, then clicking a link will lead to another http request for the server, and a new document instance is created. The client-side state will all disappear.

If your document is an SPA based on some router framework, e.g. react-router, and when you click a router-based link, no new http request will be sent, and the document remains the same one. How is this done? The onClick handlers of these links will first do e.preventDefault(), then call the History API such as history.pushState() to do same-document navigation.

How is this going to affect redux-store in an SPA site?

When you refresh the current page, a new document is created, the existing redux store will go away. After the new document is loaded, the redux store will be initialized according to your redux bootstrapping logic.

When you request a deep-link url directly in the browser, the same as above will happen.

When you click a router-based link, it will be a same-document navigation. The redux store will still be there.

When you click back/forward of the browser, then same as above will happen.

When you click a link that’s not router-based, i.e., a primitive link, the browser will just request a new document from the server side, and the existing redux store will go away

For more details about navigation in an SPA and a non-SPA, read this great article: How Single-Page Applications Work