I'm building a filter Modal in algolia. On that filter Modal, I have a standard refinementlist (see below code). When the user hits "Search" on the modal, the refinementlist values are lost (ie not applied to my component), but there is no guide on how to store refinementlist output.
What I'd like is to essentially have my Refinement list values not to clear when I close my modal.
refinementlist.js
import React from 'react';
import { RefinementList } from 'react-instantsearch-dom';
const toRefine = () => {
return (
<div>
<RefinementList
attribute={`tags`}
searchable
limit={5}
/>
</div>
);
};
export default toRefine;
filter.js
import React from 'react';
import toRefine from './toRefine';
const Filters = () => {
return (
<div>
<toRefine />
</div>
);
};
export default Filters;
MainPage.js
import React, { useState } from 'react';
import Hits from './hits';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch } from 'react-instantsearch-dom';
import Modal from 'react-modal';
import Filters from './filters';
Modal.setAppElement('#root');
const searchC = algoliasearch($ENV_VARS);
const Page = () => {
const [ modalIsOpen, setIsOpen ] = useState(false); //Hook for modal
function openModal() {
setIsOpen(true);
}
function closeModal() {
setIsOpen(false);
}
return (
<div>
<InstantSearch
indexName="index"
searchClient={searchC}
>
<CustomSearchBox />
<button onClick={openModal}>Show FILTERS</button>
<Configure hitsPerPage={20} />
<Hits />
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
contentLabel="filterElement"
className={styles.modal}
overlayClassName={styles.overlay}
>
<FilterPage />
</Modal>
</InstantSearch>
</div>
);
};
export default Page;
Each React InstantSearch widget is responsible for its UI state and needs to be mounted. I'm not familiar with react-modal, but from what I gather reading their documentation, the modal instance is destroyed when closing, not hidden, so the RefinementList is unmounted as well.
What you can do to circumvent this behavior is persist the widget's state manually whenever it changes except when closing the modal, and inject it to the widget as its default refinement.
function App() {
const [brandState, setBrandState] = React.useState([]);
// ...
return (
<InstantSearch
onSearchStateChange={(state) => {
if (modalIsOpen && state.refinementList?.brand) {
setBrandState(state.refinementList.brand);
}
}}
>
<Modal isOpen={modalIsOpen}>
<RefinementList
defaultRefinement={brandState}
attribute="brand"
/>
</Modal>
</InstantSearch>
);
}
You always need to have the RefinementList mounted in the application so that the state is persisted in React InstantSearch's internal state. You can do it declaratively by creating a virtual refinement list, which doesn't render anything, using the connectRefinementList connector.
import { connectRefinementList } from 'react-instantsearch-dom';
// ...
const VirtualRefinementList = connectRefinementList(() => null);
function App() {
// ...
return (
<InstantSearch
onSearchStateChange={(state) => {
if (modalIsOpen && state.refinementList?.brand) {
setBrandState(state.refinementList.brand);
}
}}
>
<VirtualRefinementList defaultRefinement={brandState} attribute="brand" />
{/* ... */}
</InstantSearch>
);
}
You can see it in action in this CodeSandbox demo.
Related
I have a problem with the createPortal function. When I use the "Modal", then by changing the states, the whole "Modal" component will be rendered. Can you help me how to avoid this? So when I comment out the Modal wrap in the Cart component (as I did it below), it works as I expected, but with the Modal wrap, when the states are changed, not the component will be re-rendered, but always the whole Modal Component
Here is my Modal component with createPortal function:
import React from 'react';
import { createPortal } from 'react-dom';
import { useState } from 'react';
import './modal.scss';
export default function Modal({ children, onClick }) {
const BackDrop = () => {
return <div className='backdrop' onClick={onClick}></div>;
};
const Modal = () => {
return (
<div className='modal'>
<div className='content'>{children}</div>
</div>
);
};
return (
<>
{createPortal(<BackDrop />, document.getElementById('modal'))}
{createPortal(<Modal />, document.getElementById('modal'))}
</>
);
}
The Cart component which uses the Modal component:
import React, { useEffect } from 'react';
import Modal from '../UI/Modal';
import './cart.scss';
import { useCartContext } from '../../store/CartContext';
import CartItem from './CartItem';
export default function Cart({ onHideCart }) {
const { cart, totalPrice, updateTotalPrice, addToCartOne, removeFromCartOne } = useCartContext();
useEffect(() => {
updateTotalPrice();
}, [totalPrice, cart]);
const onAddHandler = (id) => {
addToCartOne(id);
updateTotalPrice();
};
const onRemoveHandler = (id) => {
removeFromCartOne(id);
updateTotalPrice();
};
return (
// <Modal onClick={onHideCart}>
<div>
<ul className='cart-items'>
{cart.map((item, idx) => (
<CartItem
key={item.id}
name={item.name}
price={item.price}
amount={item.amount}
onAdd={onAddHandler.bind(null, item.id)}
onRemove={onRemoveHandler.bind(null, item.id)}
/>
))}
</ul>
<div className='total'>
<span>Total Amount</span>
<span>$ {totalPrice.toFixed(2)}</span>
</div>
<div className='actions'>
<button className='button--alt' onClick={onHideCart}>
Close
</button>
<button className='button'>Order</button>
</div>
</div>
// </Modal>
);
}
So by changing the amount with + and - buttons, the html element with id modal, always renders, it's flashes in the devtools... but when I comment out the Modal wrap in the Cart component, there is no flashes by modal ID. I hope it makes sense.
The problem was with the two custom element inside of the Cart element. When I return
createPortal(
<>
<div className='backdrop' onClick={onClick}></div>
<div className='modal'>
<div className='content'>{children}</div>
</div>
</>,
document.getElementById('modal')
)
instead of this:
<>
{createPortal(<BackDrop />, document.getElementById('modal'))}
{createPortal(<Modal />, document.getElementById('modal'))}
</>
Then there is no problem with rendering.
I am new to reactJS and stuck in an issue. i have a button in header that needs to toggle a class 'show' in a menu which is in some other file. I tried to use global state but do not know how to do that. here is what i did;
LAYOUT FILE
import React, { useState } from "react";
// importing header / menu etc.
function LayoutHome({ children }) {
const [state, setState] = React.useState({ MsgMenu: 'messages-dropdown' });
const handleOpenMsgMenu = (e) => {
e?.preventDefault();
setState({MsgMenu:'messages-dropdown show'});
};
return (
<>
<Header handleOpenMsgMenu={handleOpenMsgMenu} />
<MessageMenu handleOpenMsgMenu={state.MsgMenu} />
{children}
<Footer />
</>
);
}
HEADER
import React, { useState } from "react";
function Header({handleOpenMsgMenu}) {
<button type="button" onClick={handleOpenMsgMenu} className="header-notification-btn">MENU</button >
}
MENU
import React, { useState } from "react";
function MessageMenu({handleOpenMsgMenu}) {
<div id="messages-dropdown" className={handleOpenMsgMenu}>
// CONTENT
</div>
}
To achieve this you can use useState hook to toggle the display of the Menu.
create a new toggle state in global and pass it onto the menu component.
below is the complete code.
import React from "react";
export default function App({children}) {
const [state, setState] = React.useState({ MsgMenu: 'messages-dropdown' });
const [toggle, setToggle] = React.useState(false);
const handleOpenMsgMenu = (e) => {
e?.preventDefault();
setToggle(!toggle);
};
return (
<>
<Header handleOpenMsgMenu={handleOpenMsgMenu} />
<MessageMenu handleOpenMsgMenu={state.MsgMenu} toggle={toggle} />
{children}
</>
);
}
// Header
import React from "react";
function Header({handleOpenMsgMenu}) {
return <button type="button" onClick={handleOpenMsgMenu} className="header-notification-btn">MENU</button >
}
// Menu
import React from "react";
function MessageMenu({handleOpenMsgMenu, toggle}) {
return <div id="messages-dropdown" style={{display: toggle?"block":"none"}}>
<ul>
<li>
{handleOpenMsgMenu}
</li>
</ul>
</div>
}
You can toggle state with !value and then change your class depending on that value
setMenu(() => {
return {
...menu,
show: !menu.show // toggle
};
});
I've made a sample here
For the global state, check out Context or Redux
I have a Navbar component with an anchor tag containing an onClick event. On click, a value (navvalue) is passed to the function Testfunction, which is a separate component. I want to import Testfunction into the Content component so that I can have access and display the value coming from Navbar (navvalue). How do I access “navvalue” in Content? This is an assignment in a react course I´m taking. I should use props. I´m not supposed to use either state or React Route since we haven´t reach those topics yet. Thank you for your help!
Here´s my code:
App.js
import "./styles.css";
import React from "react";
import Navbar from "./Navbar";
import Content from "./Content";
import Testfunction from "./Testfunction";
export default function App() {
return (
<div className="App">
<Navbar onPageChange={Testfunction} />
<Content />
</div>
);
}
Navbar.js
import React from "react";
const Navbar = (props) => {
const navvalue = "Nav Value";
return (
<a
className="nav-link active text-uppercase"
aria-current="page"
href="#"
onClick={() => props.onPageChange(navvalue)}
>
{navvalue}
</a>
);
};
export default Navbar;
Testfunction.js
const Testfunction = (navvalue) => {
return navvalue;
};
export default Testfunction;
Content.js
import React from "react";
import Testfunction from "./Testfunction";
const Content = () => {
const navvalue = Testfunction();
return (
<p>Here´s the content. Insert value coming from Navbar here: {navvalue}</p>
);
};
export default Content;
To share some state in both <Navbar /> and <Content /> you can put state to their parent -> App. Also to <Navbar /> we pass setter function to update state which is in parent. So it can be like this:
import React from "react";
export default function App() {
const [navValue, setNavValue] = React.useState();
return (
<div className="App">
<Navbar setNavValue={setNavValue} />
<Content navValue={navValue} />
</div>
);
}
const Navbar = ({ setNavValue }) => {
const value = "Nav Value";
return <button onClick={() => setNavValue(value)}>{value}</button>;
};
const Content = ({ navValue }) => {
return (
<p>Here´s the content. Insert value coming from Navbar here: {navValue}</p>
);
};
https://codesandbox.io/s/react-mobx-change-value-in-several-components-f2tuu
How to change a variable from different places?
The variable is one, but it changes separately in different components and these changes made in one component are not reflected in the other.
App.js
import AppStore from "./AppStore";
import { observer } from "mobx-react";
import SomeComponent from "./SomeComponent";
const store = AppStore();
const App = observer(() => {
return (
<div className="App">
<div>
<i>This is text in App:</i> {store.text}
</div>
<div>
<button
onClick={() => {
store.changeText("This is text from App");
}}
>
Change text from App
</button>
</div>
<SomeComponent />
</div>
);
});
export default App;
SomeComponent.js
import React from "react";
import AppStore from "./AppStore";
import { observer } from "mobx-react";
const store = AppStore();
const SomeComponent = observer((props) => {
return (
<div>
<div>
<i>This is text in component:</i> {store.text}
</div>
<div>
<button
onClick={() => store.changeText("This is text from component")}
>
Change text from component
</button>
</div>
</div>
);
});
export default SomeComponent;
AppStore.js
import { action, makeObservable, observable } from "mobx";
export default function AppStore() {
return makeObservable(
{
text: "Initial text",
changeText(data) {
this.text = data + ": " + new Date().toLocaleTimeString();
}
},
{
text: observable,
changeText: action
}
);
}
I found sulution with React Context.
Sorry for not adding here right away.
Unfortunately, it is difficult to understand the Mobx documentation.
https://codesandbox.io/s/react-mobx-change-value-in-several-components-forked-using-react-context-e1mjh
First, we need to make context and create provider:
https://codesandbox.io/s/react-mobx-change-value-in-several-components-forked-using-react-context-e1mjh?file=/src/AppStoreProvider.js
import { useLocalStore } from "mobx-react";
import React from "react";
import AppStore from "./AppStore";
export const AppStoreContext = React.createContext();
export const AppStoreProvider = ({ children }) => {
const store = useLocalStore(AppStore);
return (
<AppStoreContext.Provider value={store}>
{children}
</AppStoreContext.Provider>
);
};
export const UseAppStore = () => React.useContext(AppStoreContext);
export default AppStoreProvider;
Where the AppStore.js is:
import { action, makeObservable, observable } from "mobx";
export default function AppStore() {
return makeObservable(
{
text: "Initial text",
changeText(data) {
this.text = data + ": " + new Date().toLocaleTimeString();
}
},
{
text: observable,
changeText: action
}
);
}
https://codesandbox.io/s/react-mobx-change-value-in-several-components-forked-using-react-context-e1mjh?file=/src/AppStore.js
Then, we need to wrap our main render in index.js with :
import { StrictMode } from "react";
import ReactDOM from "react-dom";
import AppStoreProvider from "./AppStoreProvider";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<AppStoreProvider>
<App />
</AppStoreProvider>,
rootElement
);
https://codesandbox.io/s/react-mobx-change-value-in-several-components-forked-using-react-context-e1mjh?file=/src/index.js
And then we can use context:
import "./styles.css";
import React from "react";
import { UseAppStore } from "./AppStoreProvider";
import { observer } from "mobx-react";
import SomeComponent from "./SomeComponent";
const App = observer(() => {
const store = UseAppStore();
return (
<div className="App">
<div>
<i>This is text in App:</i> {store?.text}
</div>
<div>
<button
onClick={() => {
store.changeText("This is text from App");
}}
>
Change text from App
</button>
</div>
<SomeComponent />
</div>
);
});
export default App;
https://codesandbox.io/s/react-mobx-change-value-in-several-components-forked-using-react-context-e1mjh?file=/src/App.js
Or like this:
import { useContext } from "react";
import { AppStoreContext } from "./AppStoreProvider";
import { observer } from "mobx-react";
const SomeComponent = observer((props) => {
const store = useContext(AppStoreContext);
return (
<div>
<div>
<i>This is text in component:</i> {store.text}
</div>
<div>
<button onClick={() => store.changeText("This is text from component")}>
Change text from component
</button>
</div>
</div>
);
});
export default SomeComponent;
https://codesandbox.io/s/react-mobx-change-value-in-several-components-forked-using-react-context-e1mjh?file=/src/SomeComponent.js
Hope it comes in handy for someone
Inside your App component, you created an object variable store via this code const store = AppStore();. Then you use it, display it, change it and so on. Later on inside your SomeComponent component you create a NEW variable store via const store = AppStore();. Even tho the variable names are the same and using the object, these are 2 DIFFERENT variables. Hence why updating one does not update the other. Inside the DOM they are a different object. To fix it instead of creating a new object inside the SomeComponent just pass store from the parent component. Inside the App, when rendering SomeComponent pass it the store variable like so
<SomeComponent store={store} />
And inside your SomeComponent take it from props and use it
import React from "react";
import { observer } from "mobx-react";
const SomeComponent = observer(({ store }) => {
return (
<div>
<div>
<i>This is text in component:</i> {store.text}
</div>
<div>
<button onClick={() => store.changeText("This is text from component")}>
Change text from component
</button>
</div>
</div>
);
});
export default SomeComponent;
This way both components can change the same variables data.
EDIT. I forked your project and edited the code to match what I posted here so you would have a live example to analize. https://codesandbox.io/s/react-mobx-change-value-in-several-components-forked-4c7uk?file=/src/SomeComponent.js
I've got a few React functional Components that I would like to share a state. In this example two toggle buttons that would conditionally show/hide a searchbar and a navbar.
--Solution, based on the accepted answer, on the bottom--
I'm completely new to useContext() and I keep running into the following error in the console:
Uncaught TypeError: setSearchbarToggle is not a function This goes for both buttons.
Bellow I have a filtered example code. It is just for the example I use the states in one file. In real life I would re-use the states in multiple functional components.
This is my header.js
import React, { useState, useContext } from "react"
import "./header.sass"
import { Context } from "./HeaderContext"
export const Header = () => {
const headerContext = useContext(Context)
const { navbarToggle, setNavbarToggle, searchbarToggle, setSearchbarToggle } = headerContext
return (
<React.Fragment>
<div className={"sticky-top"}>
<button onClick={ () => setNavbarToggle( !navbarToggle )}> Toggle Menu </button>
<button onClick={ () => setSearchbarToggle( !searchbarToggle )}> Toggle Search </button>
{navbarToggle && <h3>Menu is showing</h3>}
{searchbarToggle && <h3>Searchbar is showing</h3>}
</div>
</React.Fragment>
)
}
export default Header
And this is my HeaderContext.jsx
import React, { createContext, useState } from "react";
import PropTypes from "prop-types";
export const Context = createContext({});
export const Provider = props => {
const {
navbarToggle: initialNavBarToggle,
searchbarToggle: initialSarchbarToggle,
children
} = props;
const [navbarToggle, setNavbarToggle] = useState(initialNavBarToggle);
const [searchbarToggle, setSearchbarToggle] = useState(initialSarchbarToggle);
const headerContext = {
navbarToggle, setNavbarToggle,
searchbarToggle, setSearchbarToggle
};
return <Context.Provider value={headerContext}>{children}</Context.Provider>;
};
export const { Consumer } = Context;
Provider.propTypes = {
navbarToggle: PropTypes.bool,
searchbarToggle: PropTypes.bool
};
Provider.defaultProps = {
navbarToggle: false,
searchbarToggle: false
};
I hope you can shed some light on this for me
--edit--
This is my code based on the accepted answer.
import React, { useContext } from "react"
import { Provider,Context } from "./HeaderContext"
export const HeaderWithContext= () => {
const headerContext = useContext(Context)
const { navbarToggle, setNavbarToggle, searchbarToggle, setSearchbarToggle } = headerContext
return (
<React.Fragment>
<div className={"sticky-top"}>
<button onClick={ () => setNavbarToggle( !navbarToggle )}> Toggle Menu </button>
<button onClick={ () => setSearchbarToggle( !searchbarToggle )}> Toggle Search </button>
{navbarToggle && <h3>Menu is showing</h3>}
{searchbarToggle && <h3>Searchbar is showing</h3>}
</div>
</React.Fragment>
)
}
export const Header = () => {
return (
<Provider>
<HeaderWithContext/>
</Provider>
)
};
One of the parent components, e.g. App, must wrap the header (or one of its ancestor components) with Context.Provider:
import { Provider } from "./HeaderContext"
...
<Provider>
<Header />
</Provider>