cubefox
cubefox

Reputation: 1301

react keep data in sync

I have a real-time filter structure on a page. The data from my inputs are kept in my State but also as a URL query so that when someone opens the page with filters in the URL the right filters are already selected.

I'm struggling to find a stable way to keep these 2 in sync. Currently, I'm getting the data from my URL on load and setting the data in my URL whenever I change my state, but this structure makes it virtually impossible to reuse the components involved and mistakes can easily lead to infinite loops, it's also virtually impossible to expand. Is there a better architecture to handle keeping these in sync?

Upvotes: 2

Views: 1292

Answers (2)

baruchiro
baruchiro

Reputation: 5841

This answer used React Hooks


You want to keep the URL with the state, you need a two way sync, from the URL to the state (when the component mount) and from the state to the URL (when you updating the filter).

With the React Router Hooks, you can get a reactive object with the URL, and use it as the state, this is one way- from URL to the component.

The reverse way- update the URL when the component changed, can be done with history.replace.

You can hide this two way in a custom hook, and it will work like the regular useState hook:

To use Query Params as state:

import { useHistory, useLocation} from 'react-router-dom'

const useQueryAsState = () => {
    const { pathname, search } = useLocation()
    const history = useHistory()

    // helper method to create an object from URLSearchParams
    const params = getQueryParamsAsObject(search)

    const updateQuery = (updatedParams) => {
        Object.assign(params, updatedParams)
        // helper method to convert {key1:value,k:v} to '?key1=value&k=v'
        history.replace(pathname + objectToQueryParams(params))
    }

    return [params, updateQuery]
}

To use Route Params as state:

import { generatePath, useHistory, useRouteMatch } from 'react-router-dom'

const useParamsAsState = () => {
    const { path, params } = useRouteMatch()
    const history = useHistory()

    const updateParams = (updatedParams) => {
        Object.assign(params, updatedParams)
        history.push(generatePath(path, params))
    }
    return [params, updateParams]
}

Note to the history.replace in the Query Params code and to the history.push in the Route Params code.

Usage: (Not a real component from my code, sorry if there are compilation issues)

const ExampleComponent = () => {
    const [{ user }, updateParams] = useParamsAsState()
    const [{ showDetails }, updateQuery] = useQueryAsState()

    return <div>
        {user}<br/ >{showDetails === 'true' && 'Some details'}
        <DropDown ... onSelect={(selected) => updateParams({ user: selected }) />
        <Checkbox ... onChange={(isChecked) => updateQuery({ showDetails: isChecked} })} />
    </div>
}

I published this custom hook as npm package: use-route-as-state

Upvotes: 0

Timofey Goncharov
Timofey Goncharov

Reputation: 1148

I would recommend managing the state of the filters in the view from query params. If you use react-router, you can use query params instead of state and in the render method get params need for view elements. After change filters you need implement redirect. For more convenience it may be better to use qs module. With this approach you will also receive a ready-made parameter for request to backend.

Example container:

const initRequestFields = {someFilterByDefault: ''};

class Example extends Component{
  constructor(props) {
    super(props);

    this.lastSearch = '';
  }

  componentDidMount() {
    this.checkQuery();
  }

  componentDidUpdate() {
    this.checkQuery();
  }

  checkQuery() {
    const {location: {search}, history} = this.props;

    if (search) {
      this.getData();
    } else {
      history.replace({path: '/some-route', search: qs.stringify(initRequestFields)});
    }
  }

  getData() {
    const {actionGetData, location: {search}} = this.props;
    const queryString = search || `?${qs.stringify(initRequestFields)}`;
    if (this.lastSearch !== queryString) {
      this.lastSearch = queryString;
      actionGetData(queryString);
    }
  }

  onChangeFilters = (values) => {
    const {history} = this.props;

    history.push({path: '/some-route', search: qs.stringify(values)});
  };

  render() {
    const {location: {search}} = this.props;

    render(<Filters values={qs.parse(search)} onChangeFilers={this.onChangeFilters} />)
  }
}

This logic is best kept in the highest container passing the values to the components.

For get more info:

Query parameters in react router

Qs module for ease work with query

If you worry about bundle size with qs module

Upvotes: 3

Related Questions