I'm handling my app's authentication on client side using localStorage, and if there is not an "user" item in localStorage, then it means the person is not logged in and should be redirected to /login page.
The problem is when for example, someone is accessing the root page for the first time and is not logged in, the person will be able to see for 1 or 2 seconds the "/" main page content, which should only be seen by logged users, and THEN, the user will be redirected to "/login" for authentication.
I know why this is happening but I wonder if there is a better way to do this and avoid this annoying little problem.
Here's my login.js page code:
import React from "react";
import Image from "next/image";
import GoogleLogin from "react-google-login";
import { FcGoogle } from "react-icons/fc";
import { useRouter } from "next/router";
import axios from "axios";
const Login = () => {
const router = useRouter();
const apiUserLogin = async (userName, userId, userImage) => {
try {
axios.post(
`/api/users/login?name=${userName}&googleId=${userId}&imageUrl=${userImage}`
);
} catch (error) {
console.log(error);
}
};
const responseGoogle = (response) => {
localStorage.setItem("user", JSON.stringify(response.profileObj));
const { name, googleId, imageUrl } = response.profileObj;
apiUserLogin(name, googleId, imageUrl).then(() => {
router.push("/");
});
};
return (
<div className="flex flex-col items-center justify-center h-screen bg-black">
<video
src="/PostItvideo2.mp4"
type="video/mp4"
loop
controls={false}
muted
autoPlay
className="object-cover w-full h-full brightness-50"
/>
<div className="absolute top-0 bottom-0 left-0 right-0 flex flex-col items-center justify-center">
<div className="p-3">
<Image
src="/Postit-logofull.svg"
width={140}
height={80}
alt="logo"
/>
</div>
<GoogleLogin
clientId={`${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}`}
render={(renderProps) => (
<button
type="button"
className="flex items-center justify-center px-8 py-3 text-lg font-bold text-black bg-white border-none rounded-lg outline-none cursor-pointer"
onClick={renderProps.onClick}
>
<FcGoogle size={21} className="mr-2" /> Sign in with Google
</button>
)}
onSuccess={responseGoogle}
onFailure={responseGoogle}
cookiePolicy="single_host_origin"
/>
</div>
</div>
);
};
export default Login;
This is my _app.js code, this is where the "redirect if not logged in" happens:
import React, { useEffect } from "react";
import { AppContext } from "../context/context";
import { useRouter } from "next/router";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const userInfo =
localStorage.getItem("user") !== "undefined"
? JSON.parse(localStorage.getItem("user"))
: null;
if (router.pathname !== "/login" && !userInfo) router.push("/login");
}, []);
return (
<AppContext>
<Component {...pageProps} />
</AppContext>
);
}
export default MyApp;
Typically there would be a loading screen.
function MyApp({ Component, pageProps }) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const userInfo = GET_USER_FROM_LS;
if (router.pathname !== "/login" && !userInfo) {
router.push("/login");
} else {
setIsLoading(false)
}
}, []);
if(isLoading) {
return <YourLoadingComponent />
}
return YOUR_DEFAULT_RETURN;
}
I personally try to keep _app clean so I'd probably put all of the login logic in AppContext.
This my old code with VUE 2 in Tabs component:
created() {
this.tabs = this.$children;
}
Tabs:
<Tabs>
<Tab title="tab title">
....
</Tab>
<Tab title="tab title">
....
</Tab>
</Tabs>
VUE 3:
How can I get some information about childrens in Tabs component, using composition API? Get length, iterate over them, and create tabs header, ...etc? Any ideas? (using composition API)
This is my Vue 3 component now. I used provide to get information in child Tab component.
<template>
<div class="tabs">
<div class="tabs-header">
<div
v-for="(tab, index) in tabs"
:key="index"
#click="selectTab(index)"
:class="{'tab-selected': index === selectedIndex}"
class="tab"
>
{{ tab.props.title }}
</div>
</div>
<slot></slot>
</div>
</template>
<script lang="ts">
import {defineComponent, reactive, provide, onMounted, onBeforeMount, toRefs, VNode} from "vue";
interface TabProps {
title: string;
}
export default defineComponent({
name: "Tabs",
setup(_, {slots}) {
const state = reactive({
selectedIndex: 0,
tabs: [] as VNode<TabProps>[],
count: 0
});
provide("TabsProvider", state);
const selectTab = (i: number) => {
state.selectedIndex = i;
};
onBeforeMount(() => {
if (slots.default) {
state.tabs = slots.default().filter((child) => child.type.name === "Tab");
}
});
onMounted(() => {
selectTab(0);
});
return {...toRefs(state), selectTab};
}
});
</script>
Tab component:
<script lang="ts">
export default defineComponent({
name: "Tab",
setup() {
const index = ref(0);
const isActive = ref(false);
const tabs = inject("TabsProvider");
watch(
() => tabs.selectedIndex,
() => {
isActive.value = index.value === tabs.selectedIndex;
}
);
onBeforeMount(() => {
index.value = tabs.count;
tabs.count++;
isActive.value = index.value === tabs.selectedIndex;
});
return {index, isActive};
}
});
</script>
<template>
<div class="tab" v-show="isActive">
<slot></slot>
</div>
</template>
Oh guys, I solved it:
this.$slots.default().filter(child => child.type.name === 'Tab')
To someone wanting whole code:
Tabs.vue
<template>
<div>
<div class="tabs">
<ul>
<li v-for="tab in tabs" :class="{ 'is-active': tab.isActive }">
<a :href="tab.href" #click="selectTab(tab)">{{ tab.name }}</a>
</li>
</ul>
</div>
<div class="tabs-details">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "Tabs",
data() {
return {tabs: [] };
},
created() {
},
methods: {
selectTab(selectedTab) {
this.tabs.forEach(tab => {
tab.isActive = (tab.name == selectedTab.name);
});
}
}
}
</script>
<style scoped>
</style>
Tab.vue
<template>
<div v-show="isActive"><slot></slot></div>
</template>
<script>
export default {
name: "Tab",
props: {
name: { required: true },
selected: { default: false}
},
data() {
return {
isActive: false
};
},
computed: {
href() {
return '#' + this.name.toLowerCase().replace(/ /g, '-');
}
},
mounted() {
this.isActive = this.selected;
},
created() {
this.$parent.tabs.push(this);
},
}
</script>
<style scoped>
</style>
App.js
<template>
<Tabs>
<Tab :selected="true"
:name="'a'">
aa
</Tab>
<Tab :name="'b'">
bb
</Tab>
<Tab :name="'c'">
cc
</Tab>
</Tabs>
<template/>
If you copy pasted same code as me
then just add to the "tab" component a created method which adds itself to the tabs array of its parent
created() {
this.$parent.tabs.push(this);
},
My solution for scanning children elements (after much sifting through vue code) is this.
export function findChildren(parent, matcher) {
const found = [];
const root = parent.$.subTree;
walk(root, child => {
if (!matcher || matcher.test(child.$options.name)) {
found.push(child);
}
});
return found;
}
function walk(vnode, cb) {
if (!vnode) return;
if (vnode.component) {
const proxy = vnode.component.proxy;
if (proxy) cb(vnode.component.proxy);
walk(vnode.component.subTree, cb);
} else if (vnode.shapeFlag & 16) {
const vnodes = vnode.children;
for (let i = 0; i < vnodes.length; i++) {
walk(vnodes[i], cb);
}
}
}
This will return the child Components. My use for this is I have some generic dialog handling code that searches for child form element components to consult their validity state.
const found = findChildren(this, /^(OSelect|OInput|OInputitems)$/);
const invalid = found.filter(input => !input.checkHtml5Validity());
I made a small improvement to Ingrid Oberbüchler's component as it was not working with hot-reload/dynamic tabs.
in Tab.vue:
onBeforeMount(() => {
// ...
})
onBeforeUnmount(() => {
tabs.count--
})
In Tabs.vue:
const selectTab = // ...
// ...
watch(
() => state.count,
() => {
if (slots.default) {
state.tabs = slots.default().filter((child) => child.type.name === "Tab")
}
}
)
I had the same problem, and after doing so much research and asking myself why they had removed $children, I discovered that they created a better and more elegant alternative.
It's about Dynamic Components. (<component: is =" currentTabComponent "> </component>).
The information I found here:
https://v3.vuejs.org/guide/component-basics.html#dynamic-components
I hope this is useful for you, greetings to all !!
I found this updated Vue3 tutorial Building a Reusable Tabs Component with Vue Slots very helpful with explanations that connected with me.
It uses ref, provide and inject to replace this.tabs = this.$children; with which I was having the same problem.
I had been following the earlier version of the tutorial for building a tabs component (Vue2) that I originally found Creating Your Own Reusable Vue Tabs Component.
With script setup syntax, you can use useSlots: https://vuejs.org/api/sfc-script-setup.html#useslots-useattrs
<script setup>
import { useSlots, ref, computed } from 'vue';
const props = defineProps({
perPage: {
type: Number,
required: true,
},
});
const slots = useSlots();
const amountToShow = ref(props.perPage);
const totalChildrenCount = computed(() => slots.default()[0].children.length);
const childrenToShow = computed(() => slots.default()[0].children.slice(0, amountToShow.value));
</script>
<template>
<component
:is="child"
v-for="(child, index) in childrenToShow"
:key="`show-more-${child.key}-${index}`"
></component>
</template>
A per Vue documentation, supposing you have a default slot under Tabs component, you could have access to the slot´s children directly in the template like so:
// Tabs component
<template>
<div v-if="$slots && $slots.default && $slots.default()[0]" class="tabs-container">
<button
v-for="(tab, index) in getTabs($slots.default()[0].children)"
:key="index"
:class="{ active: modelValue === index }"
#click="$emit('update:model-value', index)"
>
<span>
{{ tab.props.title }}
</span>
</button>
</div>
<slot></slot>
</template>
<script setup>
defineProps({ modelValue: Number })
defineEmits(['update:model-value'])
const getTabs = tabs => {
if (Array.isArray(tabs)) {
return tabs.filter(tab => tab.type.name === 'Tab')
} else {
return []
}
</script>
<style>
...
</style>
And the Tab component could be something like:
// Tab component
<template>
<div v-show="active">
<slot></slot>
</div>
</template>
<script>
export default { name: 'Tab' }
</script>
<script setup>
defineProps({
active: Boolean,
title: String
})
</script>
The implementation should look similar to the following (considering an array of objects, one for each section, with a title and a component):
...
<tabs v-model="active">
<tab
v-for="(section, index) in sections"
:key="index"
:title="section.title"
:active="index === active"
>
<component
:is="section.component"
></component>
</app-tab>
</app-tabs>
...
<script setup>
import { ref } from 'vue'
const active = ref(0)
</script>
Another way is to make use of useSlots as explained in Vue´s documentation (link above).
Based on the answer of #Urkle:
/**
* walks a node down
* #param vnode
* #param cb
*/
export function walk(vnode, cb) {
if (!vnode) return;
if (vnode.component) {
const proxy = vnode.component.proxy;
if (proxy) cb(vnode.component.proxy);
walk(vnode.component.subTree, cb);
} else if (vnode.shapeFlag & 16) {
const vnodes = vnode.children;
for (let i = 0; i < vnodes.length; i++) {
walk(vnodes[i], cb);
}
}
}
Instead of
this.$root.$children.forEach(component => {})
write
walk(this.$root, component => {})
Many thanks #Urkle
In 3.x, the $children property is removed and no longer supported. Instead, if you need to access a child component instance, they recommend using $refs. as a array
https://v3-migration.vuejs.org/breaking-changes/children.html#_2-x-syntax
I have the following code to display data from json file, i have more that 500 records i want to display 10 records per page. Here is my project in [code pen][1] . I tried react-pagination library but that doesn't work. what is the best way to do this? Open to use any library recommended -- i tried almost all of them.
here is how my code looks like
I'm sure there are a hundred different ways of doing it, but just to teach the idea of the mechanics, here is a very manual version:
{this.state.filteredData
.slice(this.state.activePage * 10, (this.state.activePage + 1) * 10)
.map(results => ( ...
))}
.....
{/*Pagination goes here */}
<button onClick={() => {this.setState({activePage: this.state.activePage - 1})}} >
prev</button>
<button onClick={() => {this.setState({activePage: this.state.activePage + 1})}} >
next</button>
That is, you take only a slice of the data before mapping it into DOM elements, and the buttons for advancing or going back just select the slice by setting the activePage state variable you already had.
You could have something along the lines of an index and offset and then create chunks of your array.
Give this a try:
import React, { Component } from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import cardData from "./response.json";
import "./style.css";
class App extends Component {
constructor() {
super();
const offset = 5;
console.log(cardData);
this.state = {
name: "React",
index: 0,
offset,
chunks: this.chunkArray(cardData.data.Table, offset)
};
}
chunkArray(inputArray, chunkSize){
console.log("inputArray:: ", inputArray);
const results = [];
while (inputArray.length) {
results.push(inputArray.splice(0, chunkSize));
}
console.log("results:: ", results);
return results;
}
handleClick(index) {
this.setState({
index
})
}
render() {
console.log(this.state.chunks);
return (
<div>
{this.state.chunks && this.state.chunks[this.state.index].map(results => (
<div className="col-sm-3">
<h3>
{results.first_name} {results.last_name}
</h3>
<h3>{results.manager}</h3>
<div className="row">
<div className="col-md-3 col-sm-6"> {results.Department}</div>
</div>
<a
to={{ pathname: `/cards/${results.id}`, state: results }}
className={`card-wrapper restore-${results.id}`}
href={`/cards/${results.id}`}
>
View Detail
</a>
</div>
))}
<br />
{ this.state.chunks && this.state.chunks.map((item, index) => <button onClick={() => this.handleClick(index)}>{index + 1}</button>) }
</div>
);
}
}
render(<App />, document.getElementById("root"));
Here's a Working Code Sample Demo for your ref.
If you're using hooks, this will work otherwise it can be easily adapted. Basically, just store the index of where you are and then get the data you need based on that index:
const [index, setIndex] = React.useState(0);
const PAGE_SIZE = 10;
const tableData = cardData.data.Table.slice(index, index + PAGE_SIZE);
const table = {tableData.map(results => (
<div className="col-sm-3">
<h3>
{results.first_name} {results.last_name}
</h3>
<h3 >{results.manager}</h3>
<div className="row">
<div className="col-md-3 col-sm-6"> {results.Department}</div>
</div>
<Link
to={{ pathname: `/cards/${results.id}`, state: results }}
className={`card-wrapper restore-${results.id}`}
>
View Detail
</Link>
</div>
))}
const navigation = (
<div>
<div disabled={index <= 0 ? true : false} onClick={() => setIndex(index - PAGE_SIZE)}>Prev</div>
<div disabled={index <= cardData.data.Table.length ? true : false} onClick={() => setIndex(index + PAGE_SIZE)}>Next</div>
</div>
);
I have a property that I need to pass to a child component, but the child component needs to be able to modify the value that was passed. It seems like the .sync modifier is built for this, but I can't seem to get it to work. Here is my code (simplified for this question):
Profile.vue
<template>
<div>
<Avatar :editing.sync="editing"></Avatar>
click to change
...
</div>
</template>
<script>
import Avatar from './profile/Avatar'
export default {
components: { Avatar },
data() {
return {
...,
editing: false,
}
},
methods: {
editAvatar() {
this.editing = true;
}
}
}
</script>
Avatar.vue
<template>
<div>
<template v-if="!editing">
<img class="avatar-preview" :src="avatar">
</template>
<template v-else>
<label v-for="image in avatars">
<img class="avatar-thumb" :src="image">
<input type="radio" name="avatar" :checked="avatar === image">
</label>
<button class="btn btn-primary">Save</button>
</template>
</div>
</template>
<script>
export default {
props: ['editing'],
data() {
return {
avatar: '../../images/three.jpg',
avatars: [
'../../images/avatars/one.jpg',
'../../images/avatars/two.jpg',
'../../images/avatars/three.jpg',
...
]
}
},
methods: {
save() {
axios.put(`/api/user/${ user.id }/avatar`, { avatar: this.avatar }
.then(() => { console.log('avatar saved successfully'); })
.catch(() => { console.log('error saving avatar'); })
.then(() => { this.editing = false; }); // ← this triggers a Vue warning
}
}
}
</script>
You are correct - the .sync modifier is built for cases just like this. However, you are not quite using it correctly. Rather than directly modifying the prop that was passed, you instead need to emit an event, and allow the parent component to make the change.
You can resolve this issue by changing the save() method in Avatar.vue like this:
...
save() {
axios.put(`/api/user/${ user.id }/avatar`, { avatar: this.avatar }
.then(() => { console.log('avatar saved successfully'); })
.catch(() => { console.log('error saving avatar'); })
.then(() => { this.$emit('update:editing', false); });
}
}
...
I did example where my react fetches data from API.
I have the following mockup
List of objects
-- Object
--- Select field
this is my OrdersList.jsx
import React from 'react';
import Order from './Order';
class OrdersList extends React.Component {
constructor(props) {
super(props)
this.state = { data: [] }
}
componentDidMount() {
$.ajax({
url: '/api/v1/orders',
success: data => this.setState({ data: data }),
error: error => console.log(error)
})
}
render() {
return (
<div className="row">
<div className="col s12">
<table className="floatThead bordered highlight">
<thead>
<tr>
<th>id</th>
<th>status</th>
</tr>
</thead>
<Order data = { this.state.data } />
</table>
</div>
</div>
)
}
}
export default OrdersList;
here is my Order.jsx (it has Item to list and ItemStatus)
import React from 'react'
class OrderStatus extends React.Component {
constructor(props) {
super(props)
this.state = { data: [] }
}
handleChange(event) {
let data = {
order: {
status: event.target.value
}
}
console.log(event)
$.ajax({
method: 'PUT',
url: `/api/v1/orders/${event.target.id}`,
data: data,
success: data => (console.log(data), this.setState({ data: data })),
error: error => console.log(error)
})
}
render() {
return (
<div className="row">
<div className="input-field">
<p>status: {this.props.data.status}</p>
<select value={ this.props.data.status } id={ this.props.data.id } onChange={ this.handleChange } className="browser-default" >
<option value='neworder'>new</option>
<option value='pendingorder'>pending</option>
<option value='sentorder'>sent</option>
<option value='completedorder'>done</option>
</select>
</div>
</div>
)
}
}
class Order extends React.Component {
constructor(props) {
super(props)
this.state = { data: [] }
}
render() {
return (
<tbody>
{
this.props.data.map(
order =>
<tr key={order.id}>
<td>
# {order.id}
</td>
<td>
<OrderStatus data={ order } />
</td>
</tr>
)
}
</tbody>
)
}
}
export default Order;
What I am not able to understand is how to update my item status on ajax callback (there is update in backend, it works just fine, but how to update my status state on child item which is being listed by map function).. Appreciate for your input!
Your code looks fine, you are just missing a bind statement.
Add the following to your constructor and it should work:
this.handleChange = this.handleChange.bind(this);
For reference check this link.