How to refer to other component in React? - reactjs

I am trying to make a simple confirmation overlay. I have an overlay component that will overlay any parent div. I want to be able to call the confirm function from any place in my app. The code below works, but I feel like I am going against the React philosophy. My main questions are:
What is the alternative of using getElementById('main')? At the moment I want to overlay the same div, which I have given the 'main' id. This div is part of another React component. Can I use a ref in some way? Can I export this ref from the component I want to overlay and pass it to the confirm function?
My main App Component also uses the createRoot function. Is it wrong to use it again?
Am I using promises in the right way?
How to optimize below code?
import React from 'react'
import { createRoot } from 'react-dom/client'
import { Overlay } from './Overlay'
interface IConfirm {
text: string,
destroy: () => void,
resolve: (value: boolean | PromiseLike<boolean>) => void
}
export const Confirm = (props: IConfirm) => {
return (
<Overlay
text={props.text}
visible
>
<button onClick={() => {
props.resolve(true)
props.destroy()
}}>Yes</button>
<button onClick={() => {
props.resolve(false)
props.destroy()
}}>No</button>
</Overlay>
)
}
export default function confirm (text: string) {
const container = document.createElement('div')
const main = document.getElementById('main')
main!.appendChild(container)
const root = createRoot(container)
const destroy = () => {
root.unmount()
main?.removeChild(container)
}
const promise = new Promise<boolean>((resolve) => {
root.render(<Confirm text={text} destroy={destroy} resolve={resolve} />)
})
return promise
}
Thanks a lot.

Related

How to pass values to components using dynamic import of NextJS

I have a problem with dynamic import in Next.js. It would be great if someone could give me an answer or some advice to do this in a different way.
The thing is that I have a component that renders a leaflet-map, this map have a pointer so I could click the map and have longitude and latitude, this is the code:
import React from 'react'
import {MapContainer, Marker,TileLayer, useMapEvents } from 'react-leaflet'
import { iconMap } from '../../assets/customeIcon/iconMap';
import 'leaflet/dist/leaflet.css'
const MapView =({selectedPosition,setSelectedPosition}) =>{
const [initialPosition, setInitialPosition] = React.useState([38,-101]);
const Markers = () => {
const map = useMapEvents({
click(e) {
setSelectedPosition([
e.latlng.lat,
e.latlng.lng
]);
},
})
return (
selectedPosition ?
<Marker
key={selectedPosition[0]}
position={selectedPosition}
interactive={false}
icon={iconMap}
/>
: null
)
}
return <MapContainer center={selectedPosition || initialPosition} zoom={5} style={{height:"300px",width:"540px"}}>
<TileLayer url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
></TileLayer>
<Markers />
</MapContainer>
}
export default MapView
As you can see this component has the parameters selectedPosition and setSelectedPosition. This is where I save the clicked position and return it to the parent component.
For example, the parent component used to call the map component this way:
const Parent = () => {
const [selectedPosition, setSelectedPosition] = React.useState(null);
...
<MapView selectedPosition={selectedPosition} setSelectedPosition={setSelectedPosition} > </MapView>
}
This used to work great, but now because of a problem with react-leaflet I have to call the map in a different way, using Next.js dynamic import, I had to create a new component that is like this:
import dynamic from 'next/dynamic';
function MapCaller() {
const Map = React.useMemo(() => dynamic(
() => import('./MapView'),
{ ssr: false, }
), [])
return <Map />
}
export default MapCaller
So now the parent component has to call the MapCaller instead of directly calling the MapView:
const Parent = () => {
const [selectedPosition, setSelectedPosition] = React.useState(null);
...
<MapCaller > </MapCaller>
}
With this I resolved the problem of react-leaflet, but I have other problem now, remember that I used to pass the position values to the map component, how could I do to pass that values with this new approach? How the parent component could communicate with the map to get the selected position? Is there another approach to do this?
Thanks!
Your <MapCaller> component is simply wrapping the existing <MapView>, so you could simply pass the props down to it.
const Map = dynamic(() => import('./MapView'), { ssr: false })
function MapCaller({ selectedPosition, setSelectedPosition }) {
return <Map selectedPosition={selectedPosition} setSelectedPosition={setSelectedPosition} />
}
Then use it in the parent component:
const Parent = () => {
const [selectedPosition, setSelectedPosition] = React.useState(null);
//...
<MapCaller selectedPosition={selectedPosition} setSelectedPosition={setSelectedPosition} />
}

How can i use react-toastify from hook?

I found useToast and useToastContainer, but documentation is absent and i don't understand how tu use these hooks. Can anyone provide some info about these hooks?
The toasts inherit ToastContainer’s props. Props defined on toast supersede ToastContainer’s props.
There are two ways you can use toasts in your application:
1. Define ToastContainer inside the component
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const App = () => {
notify = () => toast("Wow so easy !");
return (
<div>
<button onClick={notify}>Notify !</button>
// You can add <ToastContainer /> in root component as well.
<ToastContainer />
</div>
);
}
2. Call toast.configure() once in your app. At the root of your app is the best place.
The library will mount a ToastContainer for you if none is mounted.
import { toast } from "react-toastify";
import 'react-toastify/dist/ReactToastify.css';
// Call it once in your app. At the root of your app is the best place
toast.configure()
const App = () => {
notify = () => toast("Wow so easy !");
return (
<button onClick={notify}>Notify !</button>
);
}
You can use either of them. I prefer the 2nd method because you only need to define toast.configure() which is quite clean way to add it.
You can add configuration as per your need like below:
toast.configure({
autoClose: 8000,
draggable: false,
//etc you get the idea
});
EDIT
If you want to use toast hooks, then you must wrap your app with the ToastProvider to have access to its context elsewhere within your app.
import { ToastProvider, useToasts } from 'react-toast-notifications'
const FormWithToasts = () => {
const { addToast } = useToasts()
const onSubmit = async value => {
const { error } = await dataPersistenceLayer(value)
if (error) {
addToast(error.message, { appearance: 'error' })
} else {
addToast('Saved Successfully', { appearance: 'success' })
}
}
return <form onSubmit={onSubmit}>...</form>
}
const App = () => (
<ToastProvider>
<FormWithToasts />
</ToastProvider>
)

React: A service returning a ui component (like toast)?

Requirement: Show toast on bottom-right corner of the screen on success/error/warning/info.
I can create a toast component and place it on any component where I want to show toasts, but this requires me to put Toast component on every component where I intend to show toasts. Alternatively I can place it on the root component and somehow manage show/hide (maintain state).
What I am wondering is having something similar to following
export class NotificationService {
public notify = ({message, notificationType, timeout=5, autoClose=true, icon=''}: Notification) => {
let show: boolean = true;
let onClose = () => {//do something};
if(autoClose) {
//set timeout
}
return show ? <Toast {...{message, notificationType, onClose, icon}} /> : </>;
}
}
And call this service where ever I need to show toasts.
Would this be the correct way to achieve the required functionality?
You can use AppContext to manage the state of your toast and a hook to trigger it whenever you want.
ToastContext:
import React, { createContext, useContext, useState } from 'react';
export const ToastContext = createContext();
export const useToastState = () => {
return useContext(ToastContext);
};
export default ({ children }) => {
const [toastState, setToastState] = useState(false);
const toastContext = { toastState, setToastState };
return <ToastContext.Provider value={toastContext}>{children}</ToastContext.Provider>;
};
App:
<ToastProvider>
<App/>
<Toast show={toastState}/>
</ToastProvider>
Then anywhere within your app you can do:
import {useToastState} from 'toastContext'
const {toastState, setToastState} = useToastState();
setToastState(!toastState);

On click returns null instead of an object

It's really basic I guess. I'm trying to add onClick callback to my script & I believe I'm missing a value that would be responsible for finding the actual item.
Main script
import React from 'react';
import { CSVLink } from 'react-csv';
import { data } from 'constants/data';
import GetAppIcon from '#material-ui/icons/GetApp';
import PropTypes from 'prop-types';
const handleClick = (callback) => {
callback(callback);
};
const DownloadData = (props) => {
const { callback } = props;
return (
<>
<CSVLink
data={data}
onClick={() => handleClick(callback)}
>
<GetAppIcon />
</CSVLink>
</>
);
};
DownloadData.propTypes = {
callback: PropTypes.func.isRequired,
};
export default DownloadData;
Storybook code
import React from 'react';
import DownloadData from 'common/components/DownloadData';
import { data } from 'constants/data';
import { action } from '#storybook/addon-actions';
export default {
title: 'DownloadData',
component: DownloadData,
};
export const download = () => (
<DownloadData
data={data}
callback={action('icon-clicked')}
/>
);
So right now with this code on click in the storybook I'd get null and I'm looking for an object.
One of the potential issues I can see is that your handleClick function is stored as it is in-memory, when you import the component. That means you're keeping reference of something that doesn't exists and expects to use it when rendering the component with the callback prop.
Each instance of a component should have its own function. To fix it, move the function declaration inside the component. Like this:
const Foo = ({ callback }) => {
// handleClick needs to be inside here
const handleClick = callback => {
console.log("clicked");
callback(callback);
};
return <div onClick={() => handleClick(callback)}>Click me!</div>;
};
Check this example.
If this doesn't fix your problem, then there is something wrong with how you're implementing Storybook. Like a missing context.

Redux/Enzyme testing of nested components

I have a redux connected navigation bar component that renders two sub-components. One of these is a glorified button (not redux), the other a redux connected search bar.
In a test, I want to be able to render the button and confirm the right behaviour happens when it is clicked. However if I use shallow() then it only renders a placeholder for the button and the button itself is not available to be found and clicked. If I use mount(), then the test fails as my unit test is using the non redux export for the navigation bar, which then tries to render the child search bar component, which is redux connected - and it does not have the store to pass down.
using the non-redux export is fine for testing shallow renders, but what can I do to be able to test my navigation bar component by clicking the button - that can only be fully rendered with a mount() call?
The problem I have is in the below test, if I use shallow() then it cant find the button to simulate the click, as it has only rendered as a placeholder. If I use mount() then it fails to render the <Searchbar /> component, as that is a redux connected component and my test is passing props manually without a connected store.
Is there a way to configure my navigation bar component to pass the props through to the search bar if the store doesn't exist? Or to only conditionally deep render certain components? I only want to render the PanelTileButton, not the SearchBar
My navigation bar component
interface IControlBarProps {
includeValidated: boolean,
includeValidatedChanged: (includeValidated:boolean) => void,
}
export class ControlBar extends React.Component<IControlBarProps, {}> {
constructor(props: any) {
super(props);
}
public render() {
return <div className="Control-bar">
<div className="Control-left" >
<SearchBar />
</div>
<div className="Control-center" />
<div className="Control-right">
{this.getDashboardButton("IV", "Include Validated", this.props.includeValidated, () => this.props.includeValidatedChanged(!this.props.includeValidated))}
</div>
</div>
}
private getDashboardButton(key: string, title: string, newValue: boolean, action: (value:boolean) => void)
{
return <div className="Control-Bar-Right" key={key}>
<PanelTileButton text={title} iswide={false} highlighted={newValue}
// tslint:disable
onClick={() => action(newValue)} />
</div>
}
}
function mapStateToProps(state: IStoreState) {
return {
includeValidated: state.trade.includeValidated
};
}
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
includeValidatedChanged: (includeValidated:boolean) => {
dispatch(getIncludeValidatedChangedAction(includeValidated))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ControlBar);
My test
it('should handle clicking include validated button', () => {
const mockCallback = jest.fn();
const wrapper = mount(<ControlBar includeValidated={false} includeValidatedChanged={mockCallback} />);
expect(wrapper.find('div.Control-bar').exists()).toEqual(true);
expect(wrapper.find({highlighted: false}).exists()).toEqual(true);
const pb = wrapper.find("PanelTileButton").first();
pb.find('button').simulate('click', {preventDefault() {} });
expect(mockCallback.mock.calls.length).toBe(1);
})
You can also mock components so that you can avoid having to mock the redux store all the time when you prefer to use shallow
In the same folder as SearchBar.tsx, create a sub-folder named __mocks__, and in it place a file with the same name SearchBar.tsx (it's convention) that returns minimal html
import * as React from 'react';
export default function SearchBar() {
return <div>SearchBar</div>
}
And then in your test file
jest.mock('../SearchBar') // this detects the corresponding mock automatically
import { ControlBar } from '../ControlBar';
import { shallow } from 'enzyme';
...
it('should handle clicking include validated button', () => {
const mockCallback = jest.fn();
const wrapper = shallow(<ControlBar includeValidated={false} includeValidatedChanged={mockCallback} />);
...
})
...
For others looking into how to do this, I eventually solved it by wrapping my mounted component in a <Provider> tag and using redux-mock-store to pass a mock store down to the child components that would at least let them render.
import configureMockStore from 'redux-mock-store'
it('should handle clicking include validated button', () => {
const mockCallback = jest.fn();
const mockStore = configureMockStore([])(getTestStore());
const wrapper = mount(<Provider store={mockStore}><ControlBar includeValidated={false} includeValidatedChanged={mockCallback} /></Provider>);
expect(wrapper.find('div.Control-bar').exists()).toEqual(true);
expect(wrapper.find({highlighted: false}).exists()).toEqual(true);
const pb = wrapper.find("PanelTileButton").first();
pb.find('button').simulate('click', {preventDefault() {} });
expect(mockCallback.mock.calls.length).toBe(1);
})

Resources