I have a react component which has a Tab Component in it. This is the component:
import React from 'react';
import { RTabs, I18nText } from '#wtag/react-comp-lib';
import Profiles from './Profiles';
import Search from './Search';
import Booking from './Booking';
const pathName = window.location.href.split('/').reverse()[0];
const features = [
{
tabNum: 0,
title: (
<I18nText
id="features.platform.profiles"
className="platform-features__profiles-tab"
/>
),
content: <Profiles />,
url: '/en-US/p/features/applications/profiles',
},
{
tabNum: 1,
title: (
<I18nText
id="features.platform.search"
className="platform-features__search-tab"
/>
),
content: <Search />,
url: '/en-US/p/features/applications/search',
},
{
tabNum: 2,
title: (
<I18nText
id="features.platform.booking"
className="platform-features__booking-tab"
/>
),
content: <Booking />,
url: '/en-US/p/features/applications/booking',
},
];
const getItems = () => {
return features.map(({ tabNum, title, content, url }, index) => ({
tabNum,
key: index,
title,
content,
url,
}));
};
const PlatformFeatures = () => {
return (
<div className="platform-features__tabs">
<RTabs isVertical={true} items={getItems()} selectedTabKey={2} />
</div>
);
};
export default PlatformFeatures;
When the component is loaded in the browser the first tab is selected and opened by default. While clicking on the respective tabs, the tab opens. The selected tab index number can be passed to selectedTabKey props of the RTabs component and that particular tab will be selected and opened. As seen here index 2 is passed so the 3rd tab i.e: Booking will be selected and opened by default. Now I want to achieve a functionality that the selected tab will be determined by matching the current URL it is in. Like if the URL has booking in it, the Booking tab will be opened while the browser loads the component. It will work like if I give the URL with booking in it to someone and if he pastes that URL in the browser the booking tab will be selected and opened not the default first tab. I think if I can write a function which can determine the browser URL and match it with the urls in the features array and if url matches, it will take the matched tab index from the array and pass it to the selectedTabKey props, then it might open the selected tab dynamically depending on the location of the browser url.selectedTabKey props will always take number as a PropType. I need suggestions and code examples to implement these functionalities.
const browserURL = document.location.pathname;
const filteredURL = features.map(({ url }) => url);
const checkURL = (arr, val) => {
return arr.filter(function(arrVal) {
return val === arrVal;
})[0];
};
const matchedURL = checkURL(filteredURL, browserURL);
const getSelectedTabKey = filteredURL.indexOf(matchedURL);
and then pass the getSelectedTabKey to selectedTabKey props
Related
I found myself stupid and could not get my head around with the logic.
I would like to show a few info triggered byonClick, and only those with matched id.
For example, if I click on the button with id of 1, it would only want to show values in that specific object with id:1 like description, library, etc. Right now, all the data are displayed, and because I am using component in material ui, every drawer component are displayed on top of each other (overlapping).
I know the reason causing this is because I have the drawer component inside the map method, but what could be potential solution?
Below are my simple code,
The structure of my data looks like this,
export const projectdata = [
{
id: 1,
title: "",
subtitle: "",
thumbnail: "",
description:
"",
tech: [
],
library: [""],
WebsiteUrl: "",
GitHubUrl: "",
},
...more data with sequential id number...
]
Original:
const handleDrawerOpen = (event) => () => {
setOpen(!open);
};
...
...
<SwipeableDrawer
open={open}
onClose={handleDrawerOpen(false)}
onOpen={handleDrawerOpen(true)}>
***...only show data with matched id...***
</SwipeableDrawer>
I have already map the array to display the data on webpage inside a div like this,
<div>
{ projectdata?.map(({ id, title, subtitle, thumbnail, description, tech, WebsiteUrl, GitHubUrl, library, index }) => (
<>
<Card>
...some info displayed here...
<button onClick={handleDrawerOpen(id)}></button>
</Card>
<SwipeableDrawer>
***...only show data with matched id...***
</SwipeableDrawer>
</>
))}
<div>
One solution I can think of:
with useState(), pass in id as a prop
const [projectDetailId, setProjectDetailId] = useState(null);
const [projectDetailPage, setProjectDetailPage] = useState(false);
const handleDrawerOpen = (id) => {
setProjectDetailId(id);
setProjectDetailPage(true);
};
const handleDrawerClose = () => {
setProjectDetailId(null);
setProjectDetailPage(false);
};
...
...
{projectDetailId === id ?
<SwipeableDrawer
open={projectDetailPage}
onClose={handleDrawerClose}
></SwipeableDrawer>
: null
}
However, this will trigger strange behavior of the drawer (lagging and no animation), especially with mobile device.
Possibly due to this logic projectDetailId === id ? true : false.
Ok, after your update, your problem is that you create multiple drawers, one for each id. When you click on open and set the open prop to true, all your drawers use this same prop so they all open.
You should move the Drawer out of the for and only create one, and send your object that has the id as a prop to the content of the drawer you have.
something like:
const handleDrawerOpen = (yourData) => () => {
setOpen(!open);
setYourData(yourData)
};
...
// and somewhere on your code
<SwipeableDrawer open={open}>
<SomeComponentToShowTheData data={yourData}/>
</SwipeableDrawer>
I have two pages in a Next.js project, in the first one the user fills out a form to create a post, that info gets stored in a JSON object that has to be passed to the second page so the user can see a preview of the post and if he/she likes it, the post gets created.
The object is a little to big to be passed as query parameters (it has around 25 elements in it) but I can't find how to pass it to the second page props.
The function that gives the object it's values looks like this
async function submitHandler(event) {
event.preventDefault();
validateAndSetEmail();
validateAndSetTitle();
validateAndSetSalary();
if (isFormValidated === true) {
// this checks if the user has created another post
const finalPostData = await checkExistence(Email);
if (finalPostData["user"] === "usernot found") {
setPostData({
title: Title,
name: Name,
city: City,
email: Email,
type_index: TypeNumber,
type: Type,
region_index: regionNumber,
region: region,
category_index: categoryNumber,
category: category,
range: Range,
highlighted: highlighted,
featured: featured,
show_logo: showLogo,
show_first: showFirst,
description: Description,
price: basePrice + cardPrice,
website: Website,
logo: Logo,
application_link: applicationLink,
time_to_pin: weeksToPin,
pin: pin,
post_to_google: shareOnGoogle,
});
} else {
setPostData({
// set other values to the object
});
}
// Here I want to do something like
router.push({
pathname: "/preview/",
post: PostData,
});
}
}
The second page looks something like this:
export default function PreviewPage(props) {
const item = props.postInfo; // <= I want item to recieve the JSON object from the other page
return (
<Fragment>
<Navbar />
<div className="flex inline mt-12 ml-24">
<Card
category={item.category}
Name={item.company}
logo={item.logo}
City={item.company_city}
Website={item.company_website}
/>
<Description
title={item.job_title}
description={item.description}
category={item.category}
region={item.region}
Type={item.type}
Link={item.link}
/>
</div>
<Footer />
</Fragment>
);
}
Try
router.push({
pathname: "/preview/",
query: PostData,
});
export default function PreviewPage() {
const router = useRouter()
const item = router.query
After looking into React Context and Redux thanks to knicholas's comment, I managed to find a solution.
Created a context provider file
I create the Context with 2 things inside:
An empty object representing the previewData that will be filled later
A placeholder function that will become the setter for the value
import React from "react";
const PreviewContext = React.createContext({
Previewdata: {},
setPreviewData: (data) => {},
});
export default PreviewContext;
Then in _app.js
Create a useState instance to store the previewData and have a funtion to pass to the other pages in order to set the value when the form is filled, then, wrap the component with the context provider
function MyApp({ Component, pageProps }) {
const [previewData, setPreviewData] = useState({});
return (
<div className="">
<PreviewContext.Provider value={{ previewData, setPreviewData }}>
<Component {...pageProps} />
</PreviewContext.Provider>
</div>
);
}
export default MyApp;
In the form page
I get the function to set the value of previewData by using of useContext();
const { setJobPreviewData } = useContext(jobPreviewContext);
With that the value for previewData is available everywhere in the app
When I change something on my page such as checking radio buttons or switching tabs, new network requests to retrieve images are sent from the browser. I've noticed this with a couple of websites I've made, but there should never be another request; I'm not performing a fetch on changing these values in the frontend. I don't see why the images should be requested again.
I've attached a gif showing it happening in an app I'm making with React Native, although I've seen it in my React projects too. You can see the images flicker as I switch tabs and the network calls in devtools on the right, and I'm also worried about the performance impact.
How can I prevent this from happening?
For context the data flow in my app is as follows:
In App.tsx render MainStackNavigator component.
In MainStackNavigator call firebase to retrieve data (including images). Store that data in Context.
In Home.tsx render the Tabs component, but also create an array containing the tabs data, namely the name of the component to render and the component itself.
In tabs render content based on selected tab.
Home.tsx
export const Home = (): ReactNode => {
const scenes = [
{
key: "first",
component: EnglishBreakfastHome,
},
{
key: "second",
component: SecondRoute,
},
];
return (
<View flex={1}>
<Tabs scenes={scenes} />
</View>
);
};
Tabs.tsx
export const Tabs = ({ scenes }: TabsProps): ReactNode => {
const renderScenes = useMemo(() =>
scenes.reduce((map, scene) => {
map[scene.key] = scene.component;
return map;
}, {})
);
const renderScene = SceneMap(renderScenes);
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Breakfast" },
{ key: "second", title: "Herbal" },
]);
const renderTabBar = ({ navigationState, position }: TabViewProps) => {
const inputRange = navigationState.routes.map((_, i) => i);
return (
<Box flexDirection="row">
{navigationState.routes.map((route, i) => {
const opacity = position.interpolate({
inputRange,
outputRange: inputRange.map((inputIndex) =>
inputIndex === i ? 1 : 0.5
),
});
return (
// Tab boxes and styling
);
})}
</Box>
);
};
return (
<TabView // using react-native-tab-view
style={{ flexBasis: 0 }}
navigationState={{ index, routes }}
renderScene={renderScene}
renderTabBar={renderTabBar}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
);
};
Look into something like react-native-fast-image. It is even recommended by the react-native docs.
It handles cashing of images. From the docs:
const YourImage = () => (
<FastImage
style={{ width: 200, height: 200 }}
source={{
uri: 'https://unsplash.it/400/400?image=1',
headers: { Authorization: 'someAuthToken' },
priority: FastImage.priority.normal,
}}
resizeMode={FastImage.resizeMode.contain}
/>
)
The problem here wasn't image caching at all, it was state management.
I made a call to a db to retrieve images, and then stored that array of images in AppContext which wrapped the whole app. Then, everytime I changed state at all, I ran into this problem because the app was being re-rendered. There were two things I did to remove this problem:
Separate Context into separate stores rather than just using a single global state object. I created a ContentContext that contained all the images so that state was kept separate from other state changes and, therefore, re-renders weren't triggered.
I made use of useMemo, and re-wrote the cards (that you can see in the image) to only change when the main images data array changed. This means that, regardless of changes to other state variables, that component will not need to re-render until the images array changes.
Either of the two solutions should work on their own, but I used both just to be safe.
My app allows users to click on player cards in a Field section, and then for the selected player cards to appear in a Teams section. I have an array (called selectedPlayers) and initially, each element has a default player name and default player image. As the users select players, the elements in the array are replaced one-by-one by the name and image of the selected players.
The state of the array is set in a parent component and then the array is passed to a TeamsWrapper component as a prop. I then map through the array, returning a TeamsCard component for each element of the array. However, my TeamsCards are always one selection behind reality. In other words, after the first player is selected, the first card still shows the default info; after the second player is selected, the first card now reflects the first selection, but the second card still shows the default info. The code for the TeamsWrapper component is below:
import React from "react";
import "./style.css";
import TeamsCard from "../TeamsCard";
function TeamsWrapper(props) {
const { selectedPlayers } = props;
console.log('first console.log',selectedPlayers)
return (
<div className="teamsWrapper">
{selectedPlayers.map((el, i) => {
console.log('second console.log',selectedPlayers)
console.log('third console.log',el)
return (
<div key={i}>
<TeamsCard
image={el.image}
playerName={el.playerName}
/>
</div>
);
})}
</div>
);
}
export default TeamsWrapper;
I did have this working fine before when the parent was a class-based component. However, I changed it to a function component using hooks for other purposes. So, I thought the issue was related to setting state, but the console logs indicate something else (I think). After the first player is selected:
the first console log shows a correctly updated array (i.e. the first element reflects the data for the selected player, not the placeholder data)
the second console log reflects the same
but the third print still shows the placeholder data for the first element
As mentioned above, as I continue to select players, this third print (and the TeamsCards) is always one selection behind.
EDIT:
Here is the code for the parent component (Picks), but I edited out the content that was not relevant to make it easier to follow.
import React, { useState } from "react";
import { useStateWithCallbackLazy } from "use-state-with-callback";
import TeamsWrapper from "../TeamsWrapper";
import FieldWrapper from "../FieldWrapper";
const Picks = () => {
const initialSelectedPlayers = [
{ playerName: "default name", image: "https://defaultimage" },
{ playerName: "default name", image: "https://defaultimage" },
{ playerName: "default name", image: "https://defaultimage" },
{ playerName: "default name", image: "https://defaultimage" },
{ playerName: "default name", image: "https://defaultimage" },
{ playerName: "default name", image: "https://defaultimage" },
];
const [count, setCount] = useStateWithCallbackLazy(0);
const [selectedPlayers, setSelectedPlayers] = useState(
initialSelectedPlayers
);
const handleFieldClick = (props) => {
// check to make sure player has not already been picked
const match = selectedPlayers.some(
(el) => el.playerName === props.playerName
);
if (match) {
return;
} else {
setCount(count + 1, (count) => {
updatePickPhase(props, count);
});
}
};
const updatePickPhase = (props, count) => {
if (count <= 15) {
updateTeams(props, count);
}
// elseif other stuff which doesn't apply to this issue
};
const updateTeams = (props, count) => {
const location = [0, 1, 2, 5, 4, 3];
const position = location[count - 1];
let item = { ...selectedPlayers[position] };
item.playerName = props.playerName;
item.image = props.image;
selectedPlayers[position] = item;
setSelectedPlayers(selectedPlayers);
};
return (
<>
<TeamsWrapper selectedPlayers={selectedPlayers}></TeamsWrapper>
<FieldWrapper
handleFieldClick={handleFieldClick}
count={count}
></FieldWrapper>
</>
);
};
export default Picks;
Thank you for your help!
When you update the array you are mutating state (updating existing variable instead of creating a new one), so React doesn't pick up the change and only re-render when count changes, try
const updateTeams = (props, count) => {
const position = count - 1;
const newPlayers = [...selectedPlayers];
newPlayers[position] = {playerName:props.playerName, image:props.image}
setSelectedPlayers(newPlayers);
};
This way React will see it is a new array and re-render.
I'd like to know what's the best pattern to use in the following use case:
I have a list of items in my ItemList.js
const itemList = items.map((i) => <Item key={i}></Item>);
return (
<div>{itemList}</div>
)
Each of this Items has an 'EDIT' button which should open a dialog in order to edit the item.
Where should I put the Dialog code?
In my ItemList.js => making my Item.js call the props methods to open the dialog (how do let the Dialog know which Item was clicked? Maybe with Redux save the id of the item inside the STORE and fetch it from there?)
In my Item.js => in this way each item would have its own Dialog
p.s. the number of items is limited, assume it's a value between 5 and 15.
You got a plenty of options to choose from:
Using React 16 portals
This option let you render your <Dialog> anywhere you want in DOM, but still as a child in ReactDOM, thus maintaining possibility to control and easily pass props from your <EditableItem> component.
Place <Dialog> anywhere and listen for special app state property, if you use Redux for example you can create it, place actions to change it in <EditableItem> and connect.
Use react context to send actions directly to Dialog, placed on top or wherever.
Personally, i'd choose first option.
You can have your <Dialog/> as separate component inside application's components tree and let it to be displayed in a case if your application's state contains some property that will mean "we need to edit item with such id". Then into your <Item/> you can just have onClick handler that will update this property with own id, it will lead to state update and hence <Dialog/> will be shown.
UPDATED to better answer the question and more completely tackle the problem. Also, followed the suggestion by Pavlo Zhukov in the comment below: instead of using a function that returns functions, use an inline function.
I think the short answer is: The dialog code should be put alongside the list. At least, this is what makes sense to me. It doesn't sound good to put one dialog inside each item.
If you want to have a single Dialog component, you can do something like:
import React, { useState } from "react";
import "./styles.css";
const items = [
{ _id: "1", text: "first item" },
{ _id: "2", text: "second item" },
{ _id: "3", text: "third item" },
{ _id: "4", text: "fourth item" }
];
const Item = ({ data, onEdit, key }) => {
return (
<div key={key}>
{" "}
{data._id}. {data.text}{" "}
<button type="button" onClick={onEdit}>
edit
</button>
</div>
);
};
const Dialog = ({ open, item, onClose }) => {
return (
<div>
<div> Dialog state: {open ? "opened" : "closed"} </div>
<div> Dialog item: {JSON.stringify(item)} </div>
{open && (
<button type="button" onClick={onClose}>
Close dialog
</button>
)}
</div>
);
};
export default function App() {
const [isDialogOpen, setDialogOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const openEditDialog = (item) => {
setSelectedItem(item);
setDialogOpen(true);
};
const closeEditDialog = () => {
setDialogOpen(false);
setSelectedItem(null);
};
const itemList = items.map((i) => (
<Item key={i._id} onEdit={() => openEditDialog(i)} data={i} />
));
return (
<>
{itemList}
<br />
<br />
<Dialog
open={isDialogOpen}
item={selectedItem}
onClose={closeEditDialog}
/>
</>
);
}
(or check it directly on this CodeSandbox)