Skip to main content

Use Bear React Carousel to Juejin a level Carousel

· 4 min read
Imagine Chiu
Front End Engineer @ Bearests

I'm using the open-source library I previously developed, Bear React Carousel, to see how far I can replicate the functionality of the membership level carousel from the Juejin app. We had similar requirements at my previous company. Without further ado, let's get started.

Requirements

First, let's take a look at the preview results of Juejin on the mobile app

Juejin member level carousel

  • The scrollable area is limited to the card section.
  • When scrolling, the level cards should move upwards.
  • The selected level card should be centered.
  • The area below the level card should be partially obscured with a curved shape, not fully visible.
  • The level names should move synchronously with the cards.
  • As the level names move, the curved shape should also move.
  • The level name lines should move in sync.
  • When retrieving data from the API, the preset should be positioned at the selected level without any animated movement.

The carousel project can be divided into:

  • Level cards, displaying 1.2 of them and centering.
<BearCarousel
slidesPerView={1.2}
spaceBetween={20}
isCenteredSlides={true}
// ...ignore
/>
  • Level names
  • level lines both of which should display 3 and be centered.
<BearCarousel
slidesPerView={3}
isCenteredSlides={true}
// ...ignore
/>

So the development goal is to synchronize the movement of level names and level lines when moving the level cards.

import React, {useCallback, useEffect, useRef, useState} from 'react';
import BearCarousel, {TMoveEffectFn, TBearSlideItemDataList, BearSlideCard, elClassName, Controller, TOnSlideChange} from 'bear-react-carousel';

const MemberLevelWrapper = () => {
const carouselMainRef = useRef<BearCarousel>(null);
const carouselMetaRef = useRef<BearCarousel>(null);
const carouselLineRef = useRef<BearCarousel>(null);

return <>
{/* Level Card */}
<BearCarousel
ref={carouselMainRef}
syncCarouselRefs={[carouselMetaRef, carouselLineRef]}
slidesPerView={1.2}
spaceBetween={20}
isCenteredSlides={true}
// ...ignore
/>

{/* Level Name*/}
<BearCarousel
ref={carouselMetaRef}
// ...ignore
/>

{/* Level Line */}
<BearCarousel
ref={carouselLineRef}
// ...ignore
/>
</>;
};

Next, when scrolling, the level cards will move upwards, and we need to implement a new Animation Effect function

import BearCarousel, {TMoveEffectFn} from 'bear-react-carousel';

const mainMoveEffectFn: TMoveEffectFn = useCallback((percentageInfo) => {
const transformY = 40;
return {
transform: `translate(0px, ${-transformY * (percentageInfo.calcPercentage - 1)}px)`,
};
}, []);

<BearCarousel
ref={carouselMainRef}
syncCarouselRefs={[carouselMetaRef, carouselLineRef]}
moveEffect={{
moveFn: mainMoveEffectFn,
}}
// ...ignore
/>

The level names should move in an arched pattern.

const levelNameMoveEffectFn: TMoveEffectFn = useCallback((percentageInfo) => {
const transformY = -19;
return {
transform: `translate(0px, ${-transformY * (percentageInfo.calcPercentage - 1)}px)`,
};
}, []);

<BearCarousel
ref={carouselMainRef}
moveEffect={{
moveFn: levelNameMoveEffectFn,
}}
// ...ignore
/>

As for the arched section, we will ultimately use a masking technique for display. We should also disable NavButton, Pagination, and MouseMove. If you're not familiar with SVG drawing, you can use this tool.

svg-path-editor

SVG Mask for level line

<LevelLine>
<LineBearCarousel
ref={carouselLineRef}
data={lineData}
slidesPerView={3}
isCenteredSlides={true}
isEnableNavButton={false}
isEnablePagination={false}
isEnableMouseMove={false}
/>

<svg height="100%" width="100%">
<clipPath id="wave12">
{/*跟隨線*/}
<path d="M 0 4 C 175 30 175 30 356 4 L 356 2 C 175 28 175 28 0 2" stroke="black" fill="transparent"/>
</clipPath>
</svg>
</LevelLine>

SVG Mask Result fro level line

The masking for the Level Card should be the same, but it's divided into three sections.

SVG Mask 1 for Level Card SVG Mask 2 for Level Card SVG Mask 3 for Level Card

<svg height="100%" width="100%">
<clipPath id="wave10">
<path d="M 0,0 356,0 356,130 0,130" stroke="black" fill="transparent"/>
{/* 圓弧 */}
<path d="M 0 130 C 175 155 175 155 356 130" stroke="black" fill="transparent"/>
{/* 下箭頭 */}
<path d="M 152 143 L 176 153 L 178 153 L 202 143" stroke="black" fill="transparent"/>
</clipPath>
</svg>

SVG Mask Result for Level Card

Lastly, the final part is moving to the preset selected level without any animation.

const [carouselMainController, setMainController] = useState<Controller>();
const [currLevel, setCurrLevel] = useState<{lv: number,count: number}|undefined>();


useEffect(() => {
carouselMainController?.slideToPage(5, false);
}, [carouselMainController]);


<BearCarousel
ref={carouselMainRef}
syncCarouselRefs={[carouselMetaRef]}
onSlideChange={handleSlideChange}
setController={setMainController}
slidesPerView={1.2}
spaceBetween={20}
isCenteredSlides={true}
// ...ignore
/>

The finished result

Finished Demo

The current progress is presented as a file for demonstration. If you'd like to try it out, you can visit bear-react-carousel.pages.dev. That's pretty much it. If you need both sides to move, they sync with each other in the Bear React Carousel. Because they are independently synchronized, there won't be circular control. It means A controls B and C, but B doesn't automatically control C. So, you can see here that A controls B and C.

About SSR

· 2 min read
Imagine Chiu
Front End Engineer @ Bearests

In the past few days, I updated some examples to the homepage and put them on Cloudflare via SSG. I found some strange problems. They work fine in development mode, but they don't work as expected when I page build them to the cloud.

  • Using window will cause an error because there is no such variable in Nodejs.
  • Avoid SSR rendering errors during CSR hydration.
  • Originally, the ID was bound to . The style of bear-carousel_id was specified in the tag, but if the component is re-rendered, it will cause a new ID to be generated, and the style will not be set.
Error: Hydration failed because the initial UI does not match what was rendered on the server.

What changes have been made?

  • The window part has been changed to GlobalThis.window. It is not possible to use window-related items in the constructor, so there will be some changes in the logic split. Use useState in useEffect to update the state to determine if it is CSR (if it is a class component, it is this.setState and componentDidMount), and add isClientOnly to the parts that will use window. ce0537c4
  • Instead of using the ID to set the style, change it to the CSS variable method so that the style css file can use it, and delete the dependency on ulid 9b438c3

Lazy load image

· 2 min read
Imagine Chiu
Front End Engineer @ Bearests

In general, images in a carousel are loaded immediately when the page loads. However, if there are too many images, it will not only occupy the browser's download thread, but it will also slow down the loading of other resources that need to be prioritized.

Here is a more literal translation:

In the case of a general carousel, the images are loaded directly when the page is loaded. However, if there are too many images, it will not only occupy the browser's download thread, but it will also cause the resources that need to be loaded later to be loaded more slowly.

I hope this is helpful! Let me know if you have any other questions.

Normal load iamge

Abnormal situation

The img tag does not set the rendering style (width, border, etc.), causing the observer to be unable to determine whether it has entered the screen.

So in this e4e0abf Modified in 46169dc0f43a0f2cf3L183

Use Lazy Load

At this point, we need to use Lazy load, also known as lazy loading or deferred loading.

To enable lazy loading, simply turn on the isLazy property. For more information, please refer to the documentation

Lazy laod image

We can optimize your website by using Lazy load appropriately.

Swiper carousel design and how loop

· 3 min read
Imagine Chiu
Front End Engineer @ Bearests

Research carousel mechanism

swiper banner

Today, while researching whether a dynamic Swiper carousel would affect dynamic text in Loop mode, I noticed that the approach in the latest version of Swiper is somewhat different.

I’ve been directly observing the elements as they move.

Normal State

Normal State

You can see that under normal circumstances, the method is similar to the general approach of implementing a carousel.

Need Loop State

Need Loop State

We can see from the information in ‘aria-label’ that the active item is moved to the first position, then the sequence continues. For every move, we only need to activate the transition-duration animation. After the movement is completed, the animation is deactivated, and movement control should be locked until the completion of the movement.

actually not

// active 5 (is last)
1 / 5 (-1496 * 0)
2 / 5 (-1496 * 1)
3 / 5 (-1496 * 2)
4 / 5 (-1496 * 3)
5 / 5 (-1496 * 4)

// click next, active 1
// then duration 0ms, transform: -4488px,
// then duration: 900ms, transform: -5984px,
// move
2 / 5 (0)
3 / 5 (-1496px)
4 / 5 (-2992px)
5 / 5 (-4488px)
1 / 5 (-5984px)
// then duration 0ms


// click next, active 2
// then duration 0ms, transform: -4488px,
// then duration: 900ms, transform: -5984px,
3 / 5 (0)
4 / 5 (-1496px)
5 / 5 (-2992px)
1 / 5 (-4488px)
2 / 5 (-5984px)
// then duration 0ms

The speed is too fast to see the flaws

containerEl.style.transform = `translate3d(${-1904}px, 0px, 0px)`;
containerEl.style.transitionDuration = '0ms';
setTimeout(() => {
containerEl.style.transform = `translate3d(${-3808}px, 0px, 0px)`;
containerEl.style.transitionDuration = `${this._configurator.setting.moveTime}ms`;
}, 0);

So make up to move the next target sequence to the last one, then jump to the previous position, and then move to the last one

The sorting has changed, so it becomes the judgment whether it is the last or the first, and it is judged by order


What problem can be solved

I originally thought that to loop, one must copy and then instantly replace and reset. By doing so, we can avoid issues related to additional copying and the fact that objects are not identical. Each time, a new object needs to be created.

Because changing the array order requires re-Render

It is worth thinking that if the order of the array is actually changed, you can consider whether to use css order to achieve the same effect

What problems will you encounter

Loop mode, total item 5, slidePreView 3

  • When the number of Swiper’s Loop is not divisible, it seems that the mobile computing has not been handled well.

  • When sliding manually, or when the number is insufficient, or moving from the first page to the last page, there will be some challenges

Tip

As of now, the use of Clone might still be more advantageous than disadvantageous.

Ref

Test Not Clone Branch

Swiper carousel design and how loop