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?
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
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
Related
In React all props are updated and propagated to children automatically which is nice but it slows down and requires lots of optimization at some point.
So I'm building an app with SolidJS using Context + createStore patterng and I'm having problems with consuming that state.
I'd like to create AppProvider component that manages State props and Dispatch functions. The Provider will be performing all operations on appStore, implement functions and serve them all via AppContextState and AppContextDispatch providers.
Then I need to consume that data to update components that are dependent on it reactively.
Look at the code below:
/// index.tsx
import { render } from 'solid-js/web';
import { AppProvider } from '#/providers/AppProvider';
import App from './App';
render(() => (
<AppProvider>
<App />
</AppProvider>
), document.getElementById('root') as HTMLElement);
/// AppProvider.tsx
import { createContext, useContext, JSX } from 'solid-js';
import { createStore } from 'solid-js/store';
// Interfaces
interface IAppState {
isConnected: boolean;
user: { name: string; }
}
interface IAppDispatch {
connect: () => Promise<void>;
disconnect: () => Promise<void>;
}
// Initialize
const initialState = {
isConnected: false,
user: { name: '' }
}
const initialDispatch = {
connect: () => {},
disconnect: () => {}
}
// Contexts
const AppContextState = createContext<IAppState>();
const AppContextDispatch = createContext<IAppDispatch>();
export const useAppState = () => useContext(AppContextState);
export const useAppDispatch = () => useContext(AppContextDispatch);
// Provider
export const AppProvider = (props: { children: JSX.Element }) => {
const [appStore, setAppStore] = createStore<IAppState>(initialState);
async function connect() {
setAppStore("isConnected", true);
setAppStore("user", "name", 'Chad');
}
async function disconnect() {
setAppStore("isConnected", false);
setAppStore("user", "name", '');
}
return (
<AppContextState.Provider value={appStore}>
<AppContextDispatch.Provider value={{ connect, disconnect }}>
{props.children}
</AppContextDispatch.Provider>
</AppContextState.Provider>
)
}
/// App.tsx
import { useAppState, useAppDispatch } from '#/providers/AppProvider';
export default function App() {
const { user, isConnected } = useAppState();
const { connect, disconnect } = useAppDispatch();
return (
<Show when={isConnected} fallback={<button onClick={connect}>Connect</button>}>
<button onClick={disconnect}>Disconnect</button>
<h3>Your Name: {user.name}</h3>
</Show>
)
}
This component will show a button that should run the connect function and update isConnected state and make the component within <Show> block visible but it doesn't do anything.
I verified that state is being updated by logging data of appStore in connect method.
When I change the component to depend on user.name instead isConnected it works
<Show when={user.name} fallback={<button onClick={connect}>Connect</button>}>
<button onClick={disconnect}>Disconnect</button>
<h3>Your Name: {user.name}</h3>
</Show>
However my app has many components depending on various data types, including boolean that for some doesn't work in this example with SolidJS.
I'd like to know what am I doing wrong here and understand what is the best way to share state between components. I keep reading documentation and fiddling with it but this particular problem bothers me for a past few days.
Plain Values in Solid cannot be tracked
The problem here is that primitive values / variables cannot be reactive in solid. We have two ways of tracking value access: Through function calls, and through property getters/proxies (which use signals under the hood).
So, what happens when you access a store property?
const state = useAppState();
createEffect(() => {
console.log(state.isConnected)
})
In this case, the property access is occurring within the effect, so it gets tracked, and reruns when the property value updates. On the other hand, with this:
const { isConnected } = useAppState();
We are accessing the property at the top level of the component (which is untracked and not reactive in solid). So even though we use this value in a context that is reactive (like the when prop in `), we can't run any special under-the-hood tracking to set up updates.
So why did user.name work?
The reason is that stores are deeply reactive (for primitives, objects and arrays), so
const { user } = useAppState();
Means that you are eagerly accessing the user object (so if the user property changes, you won't get updated), but the properties of the user object were not accessed yet, they only get accessed further on, in <Show when={user.name}>, so the property access user.name is able to be tracked.
I need to have a component for handling settings, this component (called Settings) stores state using useState(), for example the primary color.
I need to create a single instance of this component and make it available to every component in the app. Luckily, I already pass down a state dict to every component (I'm very unsure if this is the correct way to achieve that btw), so I can just include this Settings constant.
My problem is that I don't know how to create the component for this purpose, so that I can call its functions and pass it to children.
Here is roughly what my Settings component looks like:
const Settings = (props) => {
const [primaryColor, setPrimaryColor] = useState("")
const getColorTheme = (): string => {
return primaryColor
}
const setColorTheme = (color: string): void => {
setPrimaryColor(color)
}
return null
}
export default Settings
Then I would like to be able to do something like this somewhere else in the app:
const App = () => {
const settings = <Settings />
return (
<div style={{ color: settings.getColorTheme() }}></div>
)
}
Bear in mind that I'm completely new to react, so my approach is probably completely wrong.
You can use a custom Higher Order Component(HOC) for this purpose, which is easier than creating a context(even thougn context is also a HOC). A HOC takes a component and returns a new component. You can send any data from your HOC to the received component.
const withSettings = (Component) => {
const [settings, setSettings] = useState({})
// ...
// ...
<Component {...props} settings={settings}/>
);
And you can use it like this:
const Component = ({ settings }) => {
...your settings UI
}
export default SettingsUI = withSettings(Component);
You can read more about HOCs in the official react documentation
I'm using the following method to control my header from other components. However I'm getting the old "can't perform a react state update on unmounted component" error when changing page
export const store = {
state: {},
setState(value) {
this.state = value;
this.setters.forEach(setter => setter(this.state));
},
setters: []
};
store.setState = store.setState.bind(store);
export function useStore() {
const [ state, set ] = useState(store.state);
if (!store.setters.includes(set)) {
store.setters.push(set);
}
return [ state, store.setState ];
}
My header then uses it to set a class and control if it needs to be black on white or white on black
const Header = () => {
const [type] = useStore();
render( ... do stuff )
};
And my components on page import useStore and then call setType based on a number of factors, certain layouts are one type, some others, some vary depending on API calls so there are a lot of different Components that need to call the function to set the headers state.
const Flexible = (props) => {
const [type, setType] = useStore();
if( type !== 'dark ){ setType('dark') }
... do stuff
};
The header its self is always on page, is before and outside the router and never unmounts.
This all works perfectly fine and sets the headers sate. However when I change page with React Router I get the can't set state error. I can't see why I would get this error. I first thought that the Component might be trying to run again with react router so I moved the code to set the headers state into a useEffect that only runs on initialisation but that didn't help.
You only ever add to the setters, never remove. So when a component unmounts, it will remain in the setters, and the next time some other part of the app tries to set the state, all the setters get called, including the setter for the unmounted component. This then results in the error you're seeing.
You'll need to modify your custom hook to make use of useEffect, so that you can have teardown logic when unmounting. Something like this:
export function useStore() {
const [ state, set ] = useState(store.state);
useEffect(() => {
store.setters.push(set);
return () => {
const i = store.setters.indexOf(set);
if (i > -1) {
store.setters.splice(i, 1);
}
}
}, []);
return [ state, store.setState ];
}
This error is pretty straightforward it means that you are mutating the state (calling setState) in a component that is not mounted.
This mostly happens with promises, you call a promise, then when its resolved you update the state, but if you switch the page before it resolves, when the promise is resolved it still tries to update the state of a component that now its not mounted.
The easy and "ugly" solution, is to use some parameter that you control in componentWillUnmout to check if you still need to update the state or not like this:
var mounted = false;
componentWillMount(){
mounted = true
}
componentWillUnmount(){
mounted = false
}
// then in the promise
// blabla
promise().then(response => {
if(mounted) this.setState();
})
I've created a react app driven by Apollo client and graphQL.
My schema is defined so the expected result is an array of objects ([{name:"metric 1", type:"type A"},{name:"metric 2", type:"type B"}])
On my jsx file I have the following query defined:
query metrics($id: String!) {
metrics(id: $id) {
type
name
}
}`;
I've wrapped the component with Apollo HOC like so:
export default graphql(metricsQuery, {
options: (ownProps) => {
return {
variables: {id: ownProps.id}
}
}
})(MetricsComp);
The Apollo client works fine and returns the expected list on the props in the render method.
I want to let the user manipulate the results on the client (edit / remove a metric from the list, no mutation to the actual data on the server is needed). However since the results are on the component props, I have to move them to the state in order to be able to mutate. How can I move the results to the state without causing an infinite loop?
If apollo works anything like relay in this matter, you could try using componentWillReceiveProps:
class ... extends Component {
componentWillReceiveProps({ metrics }) {
if(metrics) {
this.setState({
metrics,
})
}
}
}
something like this.
componentWillReceiveProps will be deprecated soon (reference link)
If you are using React 16 then you can do this:
class DemoClass extends Component {
state = {
demoState: null // This is the state value which is dependent on props
}
render() {
...
}
}
DemoClass.propTypes = {
demoProp: PropTypes.any.isRequired, // This prop will be set as state of the component (demoState)
}
DemoClass.getDerivedStateFromProps = (props, state) => {
if (state.demoState === null && props.demoProp) {
return {
demoState: props.demoProp,
}
}
return null;
}
You can learn more about this by reading these: link1, link2
you can use this:
import {useState} from 'react';
import {useQuery} from '#apollo/client';
const [metrics,setMetrics]=useState();
useQuery(metricsQuery,{
variables:{id: ownProps.id},
onCompleted({metrics}){
setMetrics(metrics);
}
});
I am using this starter kit https://github.com/davezuko/react-redux-starter-kit and am following some tutorials at the same time, but the style of this codebase is slightly more advanced/different than the tutorials I am watching. I am just a little lost with one thing.
HomeView.js - This is just a view that is used in the router, there are higher level components like Root elsewhere I don't think I need to share that, if I do let me know, but it's all in the github link provided above.
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { searchListing } from '../../redux/modules/search'
export class HomeView extends React.Component {
componentDidMount () {
console.log(this.props)
}
render () {
return (
<main onClick={this.props.searchListing}>
<NavBar search={this.props.search} />
<Hero/>
<FilterBar/>
<Listings/>
<Footer/>
</main>
)
}
}
I am using connect() and passing in mapStateToProps to tell the HomeView component about the state. I am also telling it about my searchListing function that is an action which returns a type and payload.
export const searchListing = (value) => {
console.log(value)
return {
type: SEARCH_LISTINGS,
payload: value
}
}
Obviously when I call the method inside the connect() I am passing in an empty object searchListing: () => searchListing({})
const mapStateToProps = (state) => {
return {
search: { city: state.search }
}
}
export default connect((mapStateToProps), { searchListing: () => searchListing({}) })(HomeView)
This is where I am stuck, I am trying to take the pattern from the repo, which they just pass 1, I think anytime that action is created the logic is just add 1 there is no new information passed from the component.
What I am trying to accomplish is input search into a form and from the component pass the users query into the action payload, then the reducer, then update the new state with the query. I hope that is the right idea.
So if in the example the value of 1 is hardcoded and passed into the connect() method, how can I make it so that I am updating value from the component dynamically? Is this even the right thinking?
You almost got it right. Just modify the connect function to pass the action you want to call directly:
const mapStateToProps = (state) => ({
search: { city: state.search }
});
export default connect((mapStateToProps), {
searchListing
})(HomeView);
Then you may use this action with this.props.searchListing(stringToSearch) where stringToSearch is a variable containing the input value.
Notice : You don't seem to currently retrieve the user query. You may need to retrieve it first and then pass it to the searchListing action.
If you need to call a function method, use dispatch.
import { searchListing } from '../../redux/modules/search';
const mapDispatchToProps = (dispatch) => ({
searchListing: () => {
dispatch(searchListing());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(HomeView);
Then, you have made the function a prop, use it with searchListing.