I'm writing a sticky header above a FlatList, I have what I want, look at the picture below, but there are two problems about it. The effect I want is:
At first, the picture's height is 170
As user scrolls up, the picture goes up as well
But when user has scrolled 100, the picture stops going up and stays there, (meaning that the bottom 70 of the picture is showing)
Meanwhile, the opacity reduces from 1 to 0.5 minimum when user scrolls
The code is below the GIF
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
const FULL_HEADER_HEIGHT = 170;
const DEFAULT_HEADER_HEIGHT = 70;
const HEADER_DIFF = FULL_HEADER_HEIGHT - DEFAULT_HEADER_HEIGHT;
class TestScreen extends Component {
constructor(props) {
super(props);
this.state = {
data: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
};
this.scrollY = new Animated.Value(0); // How many pixels scrolled
this.headerOpacity = this.scrollY.interpolate({
inputRange: [0, HEADER_DIFF, HEADER_DIFF + 1],
outputRange: [1, 0.5, 0.5]
});
this.headerPositionY = this.scrollY.interpolate({
inputRange: [0, HEADER_DIFF, HEADER_DIFF + 1],
outputRange: [0, -HEADER_DIFF, -HEADER_DIFF]
});
}
render() {
return (
<View style={{ ... }}>
<Animated.View
style={{
transform: [{ translateY: this.headerPositionY }],
opacity: this.headerOpacity
}}
>
<Image
source={...}
style={{ width: '100%', height: FULL_HEADER_HEIGHT }}
resizeMode='cover'
/>
</Animated.View>
<AnimatedFlatList
data={this.state.data}
keyExtractor={(item) => item}
renderItem={({ item }) => (...)}
onScroll={Animated.event([{ nativeEvent: contentOffset: { y: this.scrollY } }], { useNativeDriver: true })}
/>
</View>
);
}
}
export default TestScreen;
Problem 1
The animation is not smooth
Problem 2
When the image goes up, its container seems to have stayed still, when elements of FlatList disappear, they don't disappear at the bottom of the picture, they disappear at the bottom of the original picture, which is at height 170
I think I have a similar problem with react-native: 0.55.4 on an iPhone 6 (real device). I tried to add a listener to the animated value and here is what it looks like:
18:24:57.157 Object {value: -39.919418166496385}
18:24:57.175 Object {value: -39.76205524921881}
18:24:57.200 Object {value: -39.42250543249108}
18:24:57.235 Object {value: -38.74063147167402}
18:24:57.626 Object {value: -20.959873329014293}
18:24:57.720 Object {value: -15.215298200036738}
18:24:57.742 Object {value: -13.990965698770527}
...
I know that the console adds a bias in the experiment, but we can still see some big time differences between different tick times, here ~400ms (which you can definitively see happening on the screen):
18:24:57.235 Object {value: -38.74063147167402}
18:24:57.626 Object {value: -20.959873329014293}
Compared to what looks normal ~20ms (60FPS should result in ~17ms between each tick):
18:24:57.157 Object {value: -39.919418166496385}
18:24:57.175 Object {value: -39.76205524921881}
Strangely when I do the animation in the other way, everything is just fine. What's also stranger is that I use the same animation (literally the same code) to animate different objects and some do just fine (both increasing and decreasing).
Just for the sake of comparison, here is the decrementing part:
18:24:55.541 Object {value: -1.124358489855093}
18:24:55.566 Object {value: -1.804371440300843}
18:24:55.592 Object {value: -3.0104530710073063}
18:24:55.607 Object {value: -3.896255056788916}
18:24:55.630 Object {value: -5.484551217549029}
18:24:55.661 Object {value: -7.919857696074535}
18:24:55.691 Object {value: -10.712999608975895}
18:24:55.707 Object {value: -12.43977121139206}
18:24:55.725 Object {value: -14.376069524734937}
18:24:55.743 Object {value: -16.422698462753544}
18:24:55.763 Object {value: -18.833926735827166}
18:24:55.796 Object {value: -23.027717951856513}
18:24:55.832 Object {value: -27.17280894556397}
18:24:55.865 Object {value: -30.581203230966732}
18:24:55.893 Object {value: -33.16734858413125}
18:24:55.908 Object {value: -34.426713502779336}
18:24:55.928 Object {value: -35.95476927982921}
18:24:55.957 Object {value: -37.822663476915075}
18:24:55.992 Object {value: -39.40622189728781}
18:24:56.007 Object {value: -39.80367046402294}
18:24:56.030 Object {value: -40}
The maximum duration between two frames looks to be around ~40ms.
Solution
I think I found what was causing this, I had a huge component that was updating while the animation was taking place. I think this caused JS to have more work to do and I guess that gives less opportunities for the animation events to be scheduled.. I'm not sure how it works internally but there probably is a single event loop (and a single execution thread) and while another routine (here the update method from my heavy component) is running, the animation step is not executed.
You have two solutions:
Check for updates that take place in parallel of your animation and prevent them from happening by defining shouldComponentUpdate(nextProps, nextState) for any component that you think is causing this JS slowdown to happen.
Use the native driver as suggested by this article: Dropping JS thread FPS because of doing a lot of work on the JavaScript thread at the same time
Related
I was told at a previous job that I should never access HTML elements directly through means like getElementById in React TypeScript. I'm currently implementing Chart.js. For setting up the chart, I was initially using a useRef hook instead of accessing context, but now it seems like I need to grab the canvas by ID in order to instantiate it properly. I want to know if this is kosher.
I suspect something is wrong with me not using a context, because my chart data doesn't load and throws a console error: "Failed to create chart: can't acquire context from the given item"
useEffect(() => {
chart = new Chart(chartRef.current, {
type: "bar",
data: {
labels: labelsArray.map((label) => {
const date = new Date(label);
// add one because month is 0-indexed
return date.getUTCMonth() + 1 + "/" + date.getUTCDate();
}),
datasets: [
{
data: valuesArray,
backgroundColor: "#1565C0",
borderRadius: 6,
},
],
},
options: {
interaction: { mode: "index" },
onHover: (event, chartElement) => {
const target = event.native.target;
(target as HTMLInputElement).style.cursor = chartElement[0]
? "pointer"
: "default";
},
plugins: {
tooltip: {
mode: "index",
enabled: true,
},
title: {
display: true,
text: "Daily Usage Past 30 Days",
align: "start",
fullSize: true,
font: {
size: 24,
},
padding: {
bottom: 36,
},
},
},
scales: {
x: {
display: false,
},
},
elements: {
line: {
borderJoinStyle: "round",
},
},
},
});
return () => {
chart.destroy();
};
}, [labelsArray, valuesArray]);
and HTML:
<div className="mt-80 ml-12 p-8 shadow-lg border-2 border-slate-100 rounded-xl items-center">
<canvas id="chart" ref={chartRef}></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</div>
Also, per the Chart.js documentation: "To create a chart, we need to instantiate the Chart class. To do this, we need to pass in the node, jQuery instance, or 2d context of the canvas of where we want to draw the chart." Not sure how we would do this with a useRef
Yes,
It is not good practice to access dom elements directly through document API.
Because in react
virtual dom is responsible for painting/ re-rendering the UI.
State updation is the proper way to tell react to trigger re-render.
The flow is state updation -> calculate differences -> find who over is using that state -> grab those components -> re-render only those components.
virtual dom is the source of truth for react to render and update actual DOM.
Now, If you directly access some dom elements and do some operation on it, like updating, react will never know that some change has happened and it will basically break the flow of the react, in which case there will be no reason to use react.js
The flow would be accessing some dom element -> updating -> displaying.
The problem with this approach if react encounters that later what i have in virtual dom is not actual presentation in the actual dom, which will create mess.
That is the reason there is useRef hook to manipulate dom.
I have an array of questions that I'm trying to animate using Framer Motion. The shape roughly looks like:
const questionList = [
{
type: "text",
data: {
question: "What is your name?",
valid: true,
},
},
{
type: "text",
data: {
question: "How old are you?",
valid: true,
},
},
{
type: "multi",
data: {
question: "What are your favorite sports?",
responses: ["Football", "Hockey", "Tennis", "Chess"],
valid: true,
},
},
{
type: "single",
data: {
question: "What is your gender?",
responses: ["Male", "Female", "Don't know"],
valid: true,
},
},
];
I am rendering them in a list of cards using React and Tailwind like so:
export default function QuestionList() {
const [questions, setQuestions] = React.useState(questionList);
return (
<Reorder.Group values={questions} onReorder={setQuestions}>
{questions.map((question, index) => {
return (
<Reorder.Item
className="mb-4 px-4 py-6 bg-indigo-500 text-white ml-12 border-2 border-gray-100 shadow rounded-md cursor-move hover:shadow-md"
key={`question${question.type}${index}`}
value={`question${question.type}${index}`}>
{question.data.question}
</Reorder.Item>
);
})}
</Reorder.Group>
);
}
I'd like to be able to reorder the cards using Framer Motion Reorder components as described here https://www.framer.com/docs/reorder/ but every time I try the component tree crashes silently and I get a blank screen. When I reduce the questions to a flat structure like ['Question 1', 'Question 2'] etc I am able to get the re-ordering to happen. I suspected it could be something to do with the keys but playing around with that doesn't work. Grateful for any help/pointers
You shouldn't use the loop index as the key in your Reorder.Item. When you drag to reorder the item, the index (and thus the key) will change. That will make it impossible for Reorder to track which elements have been moved to where and is probably why it's crashing.
Instead use a value like a unique id property for each question. This way React (and Framer Motion) will know exactly which element was moved and can render it at the new position.
Here's a more thorough explanation:
react key props and why you shouldn’t be using index
Besides the key having to be unique, you also should set value from Reorder.Item to value={question}. If you want to generate a unique ID for each question, perhaps consider using a library such as uuidv4
Using React functional components, I have not been able to find a way to animate my chart with dynamic data received asynchronously. The sandbox below illustrates the problem with a timer simulating the asynchronous read.
https://codesandbox.io/s/basic-column-chart-in-react-canvasjs-0gfv6?fontsize=14&hidenavigation=1&theme=dark
When running the example code, you should see 5 vertical bars of increasing heights animate. Then, after 5 seconds, it switches immediately to 4 bars of descending heights. I am looking to have that update animate.
Here is some reference information I've reviewed:
CanvasJS React Demos: many of which animate on initial draw, but I couldn't find one that animates with dynamic data loaded after the initial render.
Chart using JSON Data is an demo that has dynamic data, but doesn't animate.
Reviewing the CanvasJS forum, I found a couple links, but none that address React functional components
Vishwas from Team Canvas said:
To update dataPoints dynamically and to animate chart, you can instantiate the chart, update dataPoints via chart-options and then call chart.render as shown in this updated JSFiddle.
var chart = new CanvasJS.Chart("chartContainer", {
title: {
text: "Animation test"
},
animationEnabled: true,
data: [{
type: "column",
dataPoints: []
}]
});
chart.options.data[0].dataPoints = [{ label: "Apple", y: 658 },
{ label: "Orange", y: 200 },
{ label: "Banana", y: 900 }];
chart.render();
This sample is pure JS, but I tried to adapt the principle to my React functional component. To better comport with React best practices, I incorporated the useState hook for storing the data and the useEffect hook to handle the fetch. But, alas, I couldn't get my sandbox to animate with the dynamic data.
I think the problem is that CanvasJS expects to animate only on the first render, as stated by Sanjoy in the CanvasJS forum on 7/19/2016.
I found this SO question from Jan 2015 that suggests:
My current ugly workaround is to reinstantiate the chart every time I
update just to achieve that animation effect.
I'm hopeful that the situation has improved in the last four years, but if this hack is still the best/only way to go, I need some guidance on how to reinstantiate the chart every time using a React functional component.
To force a remount of a component pass a different key when you want to remount the component
<CanvasJSChart
key={dataPoints.toString()} // force a remount when `dataPoints` change
containerProps={containerProps}
options={options}
onRef={ref => (chart.current = ref)}
/>
Working example
I found a partial answer. Full executing code is in this code sandbox, but the critical bit is to delay the initial render of the chart until a state variable indicates that the data is available:
return (
<div className="App">
{!initialized ? (
<h1> Loading...</h1>
) : (
<CanvasJSChart containerProps={containerProps} options={options} />
)}
</div>
);
This is only a partial solution because subsequent data updates still do not animate.
Both examples works fine.
You can always animate chars with some kind of calling. I use in this case setInterval.
<script src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
<script>
var chart;
window.onload = function () {
chart = new CanvasJS.Chart("chartContainer", {
title: {
text: "Animation test"
},
animationEnabled: true,
data: [{
type: "column",
dataPoints: []
}]
});
chart.options.data[0].dataPoints = [{ label: "Apple", y: 0 },
{ label: "Orange", y: 0 },
{ label: "Banana", y: 0 }];
chart.render();
}
var max = 0;
var s = {c: 0, i: 0};
function ANIMATE() {
if (typeof chart === 'undefined') return;
chart.options.data[0].dataPoints.forEach(function(item, index, array) {
if (index == s.i) {
array[index].y += 3;
s.c++;
}
if (s.c > 12) {
s.i++;
s.c = 0;
if (s.i == 15) { s.i = 0}
}
});
if (max < 12) {
chart.options.data[0].dataPoints.push({label: "apple" + Math.random(), y: 1 + Math.random() * 10});
max++;
}
chart.render()
}
setInterval(function(){
ANIMATE()
}, 1)
</script>
<div id="chartContainer" style="height: 370px; width: 100%;"></div>
React-native: 0.47.1
How would I maintain the aspect ratio for height when the width is 100%? Basically I want to auto scale the height while maintaining the aspect ratio. The images will be displayed in a FlatList (not necessarily, if something else worls I'm happy to change the FlatList). Images are displayed from an array of data which is set in the state:
export default class ImageTest extends Component {
constructor(props) {
super(props)
this.state = {
data : [
{id: 1, name: "image1", url: "http://urlOfImage.com/1.jpg"}
{id: 2, name: "image2", url: "http://urlOfImage.com/2.jpg"}
{id: 3, name: "image3", url: "http://urlOfImage.com/3.jpg"}
{id: 4, name: "image4", url: "http://urlOfImage.com/4.jpg"}
{id: 5, name: "image5", url: "http://urlOfImage.com/5.jpg"}
];
}
}
render() {
return (
<View style={styles.container}>
<FlatList
data={this.state.data}
renderItem = {({item})=>
<Text style={styles.item}>{item.name}</Text>
<Image
source={uri: item.url}
style={styles.myImageStyle} />
}
/>
</View>
)
}
}
const styles = StyleSheet.create(
myImageStyle: {
width: Dimensions.get('window').width,
height: /* WHAT DO I PUT HERE ?? */
}
)
So the images may vary in height, original height may be 700px, some maybe 1200px, or even a square image. How would I define the height for each image since they're coming from an array?
Many thanks
You can take a dimensions of the image from URI. You can loop through your array and take the dimensions for each image using static method getSize() included in Image component. Here is a documentation for this method
Example:
const preloadedImgData = []
this.state.data.forEach((img) => {
Image.getSize(img.url, (w, h) => {
img.dimensions = {w, h}
preloadedImgData.push(img)
})
})
I'm not sure if above code is 100% correct but you got the idea :)
EDIT:
Also you can use this module which should do everything for you: react-native-fit-image
- I'm not owner of this module
Been trying to get into react and was looking at react-grid-layout when I came across a bit of a roadblock. I've pasted in the example from here essentially as is, but for some reason, when I drag an element it's not sticking. The error I'm getting in the console is:
Uncaught TypeError: this.props.onLayoutChange is not a function
I'm sure it's a simple thing that I'm missing, but this is my first React project and I would appreciate some guidance.
My code is included below:
'use strict';
var React = require('react');
var _ = require('lodash');
var ResponsiveReactGridLayout = require('react-grid-layout').Responsive;
/**
* This layout demonstrates how to use a grid with a dynamic number of elements.
*/
var AddRemoveLayout = React.createClass({
getDefaultProps() {
return {
className: "layout",
cols: {lg: 12, md: 10, sm: 6, xs: 4, xxs: 2},
rowHeight: 100
};
},
getInitialState() {
return {
items: [0, 1, 2, 3, 4].map(function(i, key, list) {
return {i: i, x: i * 2, y: 0, w: 2, h: 2, add: i === list.length - 1};
}),
newCounter: 0
};
},
createElement(el) {
var removeStyle = {
position: 'absolute',
right: '2px',
top: 0,
cursor: 'pointer'
};
var i = el.add ? '+' : el.i;
return (
<div key={i} _grid={el}>
{el.add ?
<span className="add text" onClick={this.onAddItem} title="You can add an item by clicking here, too.">Add +</span>
: <span className="text">{i}</span>}
<span className="remove" style={removeStyle} onClick={this.onRemoveItem.bind(this, i)}>x</span>
</div>
);
},
onAddItem() {
console.log('adding', 'n' + this.state.newCounter);
this.setState({
// Add a new item. It must have a unique key!
items: this.state.items.concat({
i: 'n' + this.state.newCounter,
x: this.state.items.length * 2 % (this.state.cols || 12),
y: Infinity, // puts it at the bottom
w: 2,
h: 2
}),
// Increment the counter to ensure key is always unique.
newCounter: this.state.newCounter + 1
});
},
// We're using the cols coming back from this to calculate where to add new items.
onBreakpointChange(breakpoint, cols) {
this.setState({
breakpoint: breakpoint,
cols: cols
});
},
onLayoutChange(layout) {
this.props.onLayoutChange(layout);
this.setState({layout: layout});
},
onRemoveItem(i) {
console.log('removing', i);
this.setState({items: _.reject(this.state.items, {i: i})});
},
render() {
return (
<div>
<button onClick={this.onAddItem}>Add Item</button>
<ResponsiveReactGridLayout onLayoutChange={this.onLayoutChange} onBreakpointChange={this.onBreakpointChange}
{...this.props}>
{_.map(this.state.items, this.createElement)}
</ResponsiveReactGridLayout>
</div>
);
}
});
module.exports = AddRemoveLayout;
React.render(<AddRemoveLayout/>, document.getElementById('app'))
The error you are receiving is an error about a missing prop. In a react component you basically have 2 places to keep your data, in its parent and in your component itself. Your parent often has props while declaring it because those are properties you pass to the child (like an attribute in an HTML tag). Then we have the state which is data inside a component itself.
The error you are receiving is saying that we didn't get a required prop from our parent (You can also see that inside the onLayoutChange(layout) function a call is being made to the this.props.onLayoutChange(layout) method).
So basically we are missing a few props. In the example from GitHub there is a root file called test-hook.jsx (https://github.com/STRML/react-grid-layout/blob/master/test/test-hook.jsx). This root node has as a child ( the code you are trying to render directly ) in which it is passing the required function as a property.
You can either use the test-hook.jsx or you can write your own root node which has a state with the layout and the required function which updates that state (see the github example on how to do that).
So after some searching, I figured out that the example was specifying the onLayoutChange function as a placeholder. If I wanted a custom funcion, I needed to define that.
Simply removing this function altogether and using the default fixed the issue.
Remove this:
onLayoutChange(layout) {
this.props.onLayoutChange(layout);
this.setState({layout: layout});
},
#Dirk-Jan explained it well. But the proper solution IMHO is to remove the prop call:
onLayoutChange(layout) {
// this.props.onLayoutChange(layout);
this.setState({layout: layout});
},
So the meaningful part is still there. In the examples the test-hook.jsx parent has to get hold of the layout so it can display it outside of the layout container for demonstration purposes. In a real-world application we don't need that.