Next.js > How to Implement Dynamic Route Based Modals - reactjs

Is it possible to implement a dynamic route based modal in Next.js 13 (using the app directory) ?
For reference, I'm aiming to create a similar flow to how the homepage of nomadlist (and instagram) is, such that when you click on a card, a modal appears with the updated route, i.e. https://nomadlist.com/madeira, and when visiting the link link directly, you are taken to the full page.
I am also adding the list items that have modal's to my app's root directory, and I would like to have the modal route URL nested like: /users/[userId]
What I need to do is this:
When triggering the modal in my app's list view, update the current URL without refreshing the browser. (i.e. using a State / context to trigger the modal). i.e. /users/[userId]
When visiting the modal's route directly, i.e. /users/123-43249, to display a full page of the user.
I've tried a bunch of different things, but can't seem to get this to work, so I'm more curious if this is even possible before spending more time on it.
The current structure of my app is this:
// layout.tsx
export default async function RootLayout({ children }) {
return(
<html>
<head />
<body>
<ProfileModalProvider>
{children}
</ProfileModalProvider>
</body>
</html>
)
}
// page.tsx
export default async function Home() {
const users = await getUsers();
return (
<main>
<Hero />
<SearchSection />
<UserList users={users} />
<ProfileModal /> // Should I import the whole page from '/users/[userId] here?
</main>
);
}
// ViewUserModalButton.tsx (comes from the UserList.tsx)
export default function ViewProfileButton({ user }: { user: IUser }) {
return (
<Link
as={`/users/${user.id}`}
href={{
pathname: "/users/[userId]",
query: { userId: user.id },
}}
scroll={false}
shallow={true}
>
<div>
View User
</div>
</Link>
);
}
Thank you so much.

The only way I was able to implement this was to override the default behavior of the open modal function and append URL to the window's history.
const [{ shouldShowModal }, setShouldShowModal] = useProfileModal();
const toggleModal = e => {
e.preventDefault();
setShouldShowModal({ shouldShowModal: true, profile });
window.history.pushState({}, "", `/users/${profile.id}`);
}
export default function ViewProfileButton({ user }: { user: IUser }) {
return (
<Link
as={`/users/${user.id}`}
href={{
pathname: "/users/[userId]",
query: { userId: user.id },
}}
scroll={false}
shallow={true}
onClick={toggleModal}
>
<div>
View User
</div>
</Link>
);
}

Related

How to handle loading times when a user clicks a Link or NavLink in remix run

I am building an app with remix run and using nested components.
When you click a NavLink or Link that loads a component that has a loader function to load data from an api, it can be very slow to get the response and render to the user.
Ideally I would like the URL in the browser to change immediately on click and to load an animation while the component is loading.
I know how I could implement the loading animation with react and the useEffect hook, however I'm not sure how you'd do this with remix and the Link/NavLink tags.
Remix tries to emulate the browser behaviour when navigating, so it doesn't change the URL until the loader has resolved.
However, you can improve the UX by showing some loading UI with useNavigation.
export default function App() {
const navigation = useNavigation();
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
{navigation.state !== "idle" ? <div>Loading...</div> : null}
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}
If the data in your loader in extremely slow and you're unable to speed it up, you might want to show fallback UI such as a skeleton which can be done with defer.
export function loader({ params }: LoaderArgs) {
return defer({
// NOTE: `getFoo` isn't awaited
foo: getFoo()
});
}
export default function Component() {
const data = useLoaderData<typeof loader>();
return (
<main>
<h1>Foo</h1>
<Suspense fallback={<div>Skeleton UI...</div>}>
<Await
resolve={data.foo}
errorElement={
<div>Error loading foo</div>
}
>
{(foo) => (
<div>
{JSON.stringify(foo, null, 2)}
</div>
)}
</Await>
</Suspense>
</main>
);
}
The following seems to hold the answers:
import { useNavigation } from "#remix-run/react";
function SomeComponent() {
const navigation = useNavigation();
navigation.state;
navigation.location;
navigation.formData;
navigation.formAction;
navigation.formMethod;
}
You appear to be able to hook into the navigation.state which changes from idle to loading when a link/NavLink has been clicked.

How can I decently transfer props between my components?

I want to provide a transfer of props between my two components dec.
So when I want to go to my user component from my home page, I want to show the user's name.
My app.js component:
<Link
to={{
pathname: "/user",
query: {
name: user.name
}
}}
target="_blank"
>
Link
</Link>
export const getServerSideProps = async (context) => {
console.log(context.query,"asdf");
return {
props: {
name: context.query.name
}
};
};
I am trying to send the props in query to my user component using the getServerSideProps method.
my user jsx component :
import React from "react";
const User = (props) => {
const {name} = props;
return (
<>
<span>{props.name} </span>
</>
);
};
export default User;
My main and biggest problem here is not being unable to return. Because I can't see the object with the console log. How can I solve this problem?

Passing props from parent to sibling in React

I am recreating a simple React app that I have already created in Angular. The React app has two components: one (menus.js) for a side menu and a second (content.js) that will display the content from each item in the menu when each link is clicked (just like an iframe of sorts). In the App.js I am making a REST API call to populate the state for the menus.js component. Note that both components are in the App.js as follows:
App.js
import React,{Component} from 'react';
import Menus from './components/menus';
import Content from './components/content';
class App extends Component {
state = {
menus: []
}
componentDidMount(){
fetch('api address')
.then(res => res.json())
.then((data)=> {
this.setState({menus: data})
})
.catch(console.log)
}
render(){
return (
<div>
<div><Menus menus={this.state.menus} /></div>
<div><Content /></div>
</div>
);
}
}
export default App;
here is the menu.js component; it takes a prop (menus) from App.js and builds the menu links with items from it:
import React from 'react';
import { BrowserRouter as Router, Link,} from "react-router-dom";
const Menus = ({ menus }) => {
return (
<Router>
<div>
<center><h1>Lessons</h1></center>
{menus.map(menu => (
<li key={menu.lesson}>
<Link to={`/lesson/${menu.lesson}`}>{menu.lessonName}</Link>
</li>
))}
</div>
</Router>
);
};
export default Menus;
Here is what I need - how do I pass items from the same prop (from App.js) to the content component? FYI - I need this to happen each time a link in the menu in menu.js is clicked (which is why a key is used in the list The simple idea is content will update in the content component each time a menu link in the menu component is clicked.
**content.js**
import React from 'react'
const Content = () => {
return (
<div>{menu.content}</div>
)
};
export default Content
Based on your description of the problem and what I can see of what you've written, it seems to me like you are trying to build an application where the menu persists, but the content changes based on menu clicks. For a simple application, this is how I would structure it differently.
<ParentmostComponent>
<MenuComponent someProp={this.state.some_data} />
<Switch>
<Route path={"/path"} render={(props) => <Dashboard {...props} someProp={this.state.some_other_data_from_parents} />
</Switch>
</ParentMostComponent>
This would allow the menu to always stay there no matter what the content is doing, and you also won't have to pass the menu prop to two components.
In your menu.js, attach the menu object to the Link
...
{menus.map(menu => (
<li key={menu.lesson}>
<Link to={{
pathname: `/lesson/${menu.lesson}`,
state: menu
}}> {menu.lessonName} </Link>
</li>
))}
...
In your content.js receive the menu like this:
import React from 'react'
const Content = () => {
console.log(props.location.state.menu.content);
return (
<div>{props.location.state && props.location.state.menu.content }</div>
)
};
export default Content
Read more here
Your example uses React Router, so this answer uses it as well.
First of all, move the Router up the hierarchy from Menus to App to make the router props available to all components. Then wrap your Content inside a Route to render it conditionally (i.e. if the path matches "/lesson/:lesson"):
class App extends Component {
state = {
menus: [
{
lesson: '61355373',
lessonName: 'Passing props from parent to sibling in React',
content: 'I am recreating a simple React app…'
},
{
lesson: '27991366',
lessonName: 'What is the difference between state and props in React?',
content: 'I was watching a Pluralsight course on React…'
}
]
}
render() {
const { menus } = this.state
return (
<Router>
<div>
<div><Menus menus={menus}/></div>
<Route path="/lesson/:lesson" render={({ match }) => (
<div><Content menu={menus.find(menu => menu.lesson === match.params.lesson)}/></div>
)}>
</Route>
</div>
</Router>
)
}
}
With the help of the render prop, you can access the router props (in this case match.params.lesson) before rendering your child component. We use them to pass the selected menu to Content. Done!
Note: The basic technique (without React Router, Redux etc.) to pass props between siblings is to lift the state up.

React - Show only the clicked user

In the following app, I'm accessing the random user API and show a list of 12 users.
App.js
import React, { useState } from 'react'
import UserList from './components/UserList'
const App = props => {
const [id, setID] = useState(null)
console.log(`Passed variable to App.js is: ` + id)
return (
<>
<UserList setID={setID} />
</>
)
}
export default App
UserList.js
import React, { useState, useEffect } from 'react'
import axios from 'axios'
const UserList = ({ setID }) => {
const [resources, setResources] = useState([])
const fetchResource = async () => {
const response = await axios.get(
'https://api.randomuser.me/?results=12'
)
setResources(response.data.results)
}
useEffect(() => {
fetchResource()
}, [])
return (
<ul>
{resources.map(item => (
<li key={item.name.first}>
<div>
<h2>{item.name.first} {item.name.last}</h2>
<button
onClick={() => setID(item.login.uuid)}
>
Details
</button>
</div>
</li>
))}
</ul>
)
}
export default UserList
The above code is working. But now I want that if I click on the button for any of those listed users, only that user get showed.
How can I do that?
The response JSON looks like this:
Easiest way would be to apply a filter on your ressources variable to only display the user with selected uuid.
To do that, first you need to share selected id with UserList component:
App.js
<UserList id={id} setID={setID} />
Then update UserList accordingly:
UserList.js
const UserList = ({ id, setID }) => {
return (
<ul>
{ resources
.filter(user => Boolean(id) ? user.login.uuid == id : true )
.map(item => (
<li key={item.name.first}>
<div>
<h2>{item.name.first} {item.name.last}</h2>
{ Boolean(id) ?
<button onClick={() => setID(null)}>
Hide
</button>
:
<button onClick={() => setID(item.login.uuid)}>
Details
</button>
}
</div>
</li>
)
}
</ul>
)
}
That way, you will only display the select user in you <ul>. To unselect your user, just call setID(null)
Show user profile instead of list
If that solution work to filter your list, I guess you might want to adapt your page to show all details from your user. Next step would be to implement multi pages using react-router-dom with a url container your user uuid.
You can look at the url-params example which might be exactly what you are looking for.
Here's a slightly detailed option that extends beyond a single component but more easy to scale on account of modularity.
Create a new react component in a new file say, UserDetails.js
Now you need a way to navigate to this new page when the button is clicked.
So in your App.js you need a router like
import { BrowserRouter, Switch} from 'react-router-dom'
Then in your App.js file wrap all your components in the router:
class App extends Component {
render() {
return (
<BrowserRouter>
<div className="App">
<Switch>
<Route exact path="/user-list" component={UserList} />
<Route exact path="/detail" component={UserDetails}/>
</Switch>
</div>
</BrowserRouter>
);
}
}
export default App;
Now you are ready to navigate to the user details page, when the button is clicked. So add a function like goToDetails like:
<button onClick={() => goToDetails(item)}>
Next define the function that navigates to the next page
goToDetails(item) {
this.props.history.push('/detail', {selectedUser:item:});
}
The history prop is available above because we earlier wrapped the entire app in BrowserRouter.
In the details page, you get the selectedUser details as a prop:
const selectedUser = this.props.location.state.selectedUser;
Now you can render it however you want.

Redux/React - Why I can't bind state to this.props on component which is in iframe?

I have a specific scenario in my react/redux/express universal project (server-side rendering).
(1)First I defined my routes like so: [ routes.jsx ]
export default (
<Route component={App} path="/">
<Route component={MainView} path="main">
<IndexRoute component={ThemeIndex}></IndexRoute>
</Route>
<Route component={AnotherView} path="preview" />
</Route>
);
As you see, when url route is: localhost:3000/preview, react-router will use AnotherView component.
(2)Now focus on ThemeIndex component: [ ThemeIndex.jsx ]
export default class ThemeIndex extends Component {
render() {
return (
<div>
<h2>Index</h2>
<Frame />
<Control />
</div>
);
}
}
(3)Frame component like so: [ Frame.jsx ]
class Frame extends Component {
render() {
const { text, uid } = this.props.infos;
const themeUrl = `http://localhost:3000/preview?id=${uid}`;
//console.log('Frame theme:', text);
//console.log('Frame uid:', uid);
return (
<div className="col-md-8 panel panel-default">
<div className="embed-responsive embed-responsive-16by9 panel-body">
<iframe src={themeUrl}></iframe>
</div>
<div className="details" >
{text}
</div>
</div>
);
}
}
export default connect(
(state) => {
return {
infos: state.infos
}
}
)(Frame);
Here I use iframe tag, its src is http://localhost:3000/preview?id=xxxx, so it means it will link AnotherView component to be iframe's page.
(4)AnotherView Component like so:
class AnotherView extends Component {
render() {
const { text, uid } = this.props.infos;
//console.log('AnotherView theme:', text);
//console.log('AnotherView uid:', uid);
return (
<div>
<div >Another View</div>
<div>
{text}
</div>
<div>{uid}</div>
</div>
);
}
}
export default connect(
(state) => {
console.log('another view trigger state:', state);
return {
infos: state.infos
}
}
)(AnotherView);
(4)And I have Control component for making dynamic value: [ Component.jsx ]
class Control extends Component {
render(){
var uid = () => Math.random().toString(34).slice(2);
return (
<input
onChange={(event) => this.props.addTodo({text:event.target.value, uid:uid()})
/>
)
}
}
export default connect(
(state) => {
return {
infos: state.infos
}
}
)(Control);
(5)List extra files, Action and Reducer:
[ action.js ]
export function addTodo (attrs) {
return {
type: 'ADD_TODO',
attrs
};
}
[ reducer.js ]
export default (state = {text:'', uid:''}, action) => {
switch(action.type) {
case 'ADD_TODO':
return Object.assign({}, state, action.attrs);
default:
return state;
}
}
Here is Store configuration on server.js:
app.use( (req, res) => {
console.log('server - reducers:', reducers);
const location = createLocation(req.url);
const reducer = combineReducers({infos: infosReducer});
const store = applyMiddleware(promiseMiddleware)(createStore)(reducer);
match({ routes, location }, (err, redirectLocation, renderProps) => {
.......
function renderView() {
const createElement = (Component, props) => (
<Component
{...props}
radiumConfig={{ userAgent: req.headers['user-agent'] }}
/>
);
const InitialView = (
<Provider store={store}>
<RoutingContext
{...renderProps}
createElement={createElement} />
</Provider>
);
const componentHTML = renderToString(InitialView);
const initialState = store.getState();
......
my application state is like :
{
infos:{
text: '',
uid: ''
}
}
(6)Now I key some words on input in Control component. When the input onChange will trigger addTodo action function to dispatch action in reducer, finally change the state. In common, the state changing will effect Frame component and AnotherView component, because I used react-redux connect, bind the state property to this.props on the component.
But in fact, there is a problem in AnotherView component. in Frame component, console.log value display the text you key in input correctly. In AnotherView component, even the connect callback will be trigger (console.log will print 'another view trigger state: ...') , the console.log in render is undefined, like:
console.log('AnotherView theme:', text); //return AnotherView theme: undefined
console.log('AnotherView uid:', uid); //return AnotherView uid: undefined
I found the main reason: AnotherView component is in iframe. Because if I remove iframe, put AnotherView component directly here, like so:
return (
<div className="col-md-8 panel panel-default">
<div className="embed-responsive embed-responsive-16by9 panel-body">
<AnotherView/>
</div>
<div className="details" >
{text}
</div>
</div>
);
then I can bind state properties on this.props in AnotherView component successfully, then insert {text} on JSX html, you can see the value changing real time when you key input value on Control component. if I use iframe to link AnotherView component be its page, you can't see any changing {text} value, because my text default value is empty string value.
How do I bind state properties to this.props in the component which is in iframe when state changing?
Update
I can't get the latest state in iframe (source is React component), when I changing state in another component, and actually the mapStateToProps was triggered!(means iframe source component) but its state is not the latest in mapStateToProps function. it does not really concerns with the react-redux library?
This is the latest state should be in component:
Below is iframe source component, it can't get the latest state:
If you load an app in an iframe from a script tag, it will load a separate instance of the app. This is the point of iframes: they isolate code.
Two separate instances of the app won’t “see” updates from each other. It’s like if you open the app in two separate browser tabs. Unless you add some method of communication between them, they will not share state.
It is not clear why you want to render a frame rather than a component directly. But if you really need frames, a simple option would be to use to render the component into the frame rather than load a separate app there. You can use a library like react-frame-component for this. Or, simpler, you can just not use frames at all, as it is not clear what purpose they serve. Usually people want them to isolate an app instance but this seems contrary to what you seem to want.

Resources